DEV Community

Isaac Dyor
Isaac Dyor

Posted on

How to create Google Maps clone with Next.js, Prisma, and Postgres.

This article is a documentation of my process of implementing a map on my website that displays markers at certain points stored in a Postgres database through Next.js and Prisma.

To start this project I created a Next.js project with the command:

npx create-next-app@latest

Next I created a Postgres database hosted on Heroku following these steps: .

Then I needed to connect my Next project to my Postgres database through Prisma. The first step was to install Prisma with the following command:

npm install prisma --save-dev

Then I initialized the Prisma project by running

npx prisma init

This adds a prisma.schema file which is where you define your schema. It also creates a .env file where you can define your environment variables. In my .env file I defined my database link. You can find this by following step 4 of the link to setup a postgres database.

DATABASE_URL="postgresql:blahblahblah"

Then I created my schema in the prisma.schema file. Make sure to include an address field in the schema because that is how our program will know where to place the markers. I also included other information I wanted to provide the user in the info window.

//prisma.schema
datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model Location {
  id        String     @default(cuid()) @id
  title     String
  address   String?
  website   String?
  phone     String?
}
Enter fullscreen mode Exit fullscreen mode

Push the schema to your database

npx prisma db push

Install prisma client

npm install @prisma/client

Updata your prisma client

npx prisma generate

Create a new directory called lib and a prisma.js file in it.

In the prisma.js file you have to create an instance of the Prisma client.

Then you can import your instance of the Prisma client into any file you need.

//prisma.js
const { PrismaClient } = require('@prisma/client')
const prisma = new PrismaClient()

export default prisma
Enter fullscreen mode Exit fullscreen mode

Run npx prisma studio to open the Prisma studio, I added a few entries to play around with.

Now that I have my project connected with my database I can start building the webpage.

I created a new file in the pages directory called maps.js. First I imported all of the packages that we need to use. We need useState and useRef from React to manage the state.
We also need to import a few things from the @react-google-maps/api package which is a package designed to connect the google maps api to our react application.
We also need a few things from the react-places-autocomplete package which makes it easy for us to implement a google places api searchbar into our application.
I also imported my prisma instance from my prisma.js file, and the script package from next/script.

import React, {useState, useRef} from 'react';
import {GoogleMap, useLoadScript, Marker, InfoWindow,} from "@react-google-maps/api";
import PlacesAutocomplete, {geocodeByAddress, getLatLng} from 'react-places-autocomplete'

import Script from "next/script";
import prisma from "../lib/prisma";

const libraries = ['places']
Enter fullscreen mode Exit fullscreen mode

After we have all of this imported then we can query our database for our data.

export const getServerSideProps = async () => {
  const locations = await prisma.location.findMany();
  return { props: { locations } };
}
Enter fullscreen mode Exit fullscreen mode

Then we can create a new functional component with our quereyed data as a prop.

const App = ({ locations }) => {

}
Enter fullscreen mode Exit fullscreen mode

Then we are going to create some state. I created a lot of state and this can probably done in a more efficient way but it works so I will go with it.

const App = ({ locations }) => {

  const [center, setCenter] = useState({
    lat: 0,
    lng: 0,
  });

  const [address, setAddress] = useState("");

  const [coords, setCoords] = useState([]);

  const [mapRef, setMapRef] = useState(null);

  const [selected, setSelected] = useState(null);

  const mapRef2 = useRef();

  const options = {
    disableDefaultUI: true,
    zoomControl: true,
  }

}
Enter fullscreen mode Exit fullscreen mode

The mapRef2 is pretty stupid but who cares.

Next we need to connect to the google maps api. We do this through the useLoadScript function we imported earlier. The first step is to get a google maps api key. The instructions to do so can be found here.

The second step is to create a .env.local file in the root directory. You might be able to use the .env file that Prisma created but this is the way that I did it. In the .env.local file add the following line and insert your API Key.

NEXT_PUBLIC_MAPS_API_KEY=your-api-key

You can then use this api key in your component with the following function:

  const { isLoaded } = useLoadScript({
    googleMapsApiKey: process.env.NEXT_PUBLIC_MAPS_API_KEY,
    libraries,
  })
Enter fullscreen mode Exit fullscreen mode

The libraries line at the end importants the places library.

Now we need to define a few functions that will be called later on in our code.

The first function takes the address that the user selects from the places autocomplete dropdown and it converts the address to latitude and longitude. It also sets the center to the new latitude and longitude.

  const handleSelect = async (value) => {
    const results = await geocodeByAddress(value);
    const latLng = await getLatLng(results[0]);
    setAddress(value);
    setCenter(latLng);
  };
Enter fullscreen mode Exit fullscreen mode

The next function is the convertAddress function which is called onMapLoad and converts all of the addresses stored in the database to latitude and longitude points so that we can use those coordinates to display markers later on.

  const convertAddress = async (value) => {
    const results = await geocodeByAddress(value.address);
    const latLng = await getLatLng(results[0]);
    const locationData = {
      title: value.title,
      address: value.address,
      website: value.website,
      phone: value.phone,
      lat: latLng.lat,
      lng: latLng.lng
    }
    setCoords(coords => [...coords, locationData])
  };
Enter fullscreen mode Exit fullscreen mode

The next function is called when someone clicks on a marker. What this function does is set the center of the map to whatever the current center is. It gets the current center through calling getCenter() on the mapRef.

  const onCenterChanged = () => {
    if (mapRef) {
      const newCenter = mapRef.getCenter();
      console.log(newCenter);
      setCenter({
        lat: mapRef.getCenter().lat(),
        lng: mapRef.getCenter().lng()
      })
    }
  }
Enter fullscreen mode Exit fullscreen mode

The next function is called when the map loads, and it initializes the map as well as converts all of our addresses into latitude and longitude as mentioned earlier.

  const onCenterChanged = () => {
    if (mapRef) {
      const newCenter = mapRef.getCenter();
      console.log(newCenter);
      setCenter({
        lat: mapRef.getCenter().lat(),
        lng: mapRef.getCenter().lng()
      })
    }
  }
Enter fullscreen mode Exit fullscreen mode

The final function just pans the map to a certain lat and long.

  const panTo = React.useCallback(({lat, lng}) => {
    mapRef2.current.panTo({lat, lng});
  }, [])
Enter fullscreen mode Exit fullscreen mode

Overall our component looks like this right now:

const App = ({ locations }) => {

  const [center, setCenter] = useState({
    lat: 0,
    lng: 0,
  });

  const [address, setAddress] = useState("");

  const [coords, setCoords] = useState([]);

  const [mapRef, setMapRef] = useState(null);

  const [selected, setSelected] = useState(null);

  const mapRef2 = useRef();

  const options = {
    disableDefaultUI: true,
    zoomControl: true,
  }

  const { isLoaded } = useLoadScript({
    googleMapsApiKey: process.env.NEXT_PUBLIC_MAPS_API_KEY,
    libraries,
  })

  const handleSelect = async (value) => {
    const results = await geocodeByAddress(value);
    const latLng = await getLatLng(results[0]);
    setAddress(value);
    setCenter(latLng);
  };

  const convertAddress = async (value) => {
    const results = await geocodeByAddress(value.address);
    const latLng = await getLatLng(results[0]);
    const locationData = {
      title: value.title,
      address: value.address,
      website: value.website,
      phone: value.phone,
      lat: latLng.lat,
      lng: latLng.lng
    }
    setCoords(coords => [...coords, locationData])
  };

  const onCenterChanged = () => {
    if (mapRef) {
      const newCenter = mapRef.getCenter();
      console.log(newCenter);
      setCenter({
        lat: mapRef.getCenter().lat(),
        lng: mapRef.getCenter().lng()
      })
    }
  }



  const onMapLoad = (map) => {
    mapRef2.current = map
    setMapRef(map);
    {locations.map(location => {
      convertAddress(location)
    })}
  }

  const panTo = React.useCallback(({lat, lng}) => {
    mapRef2.current.panTo({lat, lng});
  }, [])
Enter fullscreen mode Exit fullscreen mode

The first thing I did was create a button that got the coordinates of the user and panned the map to those coordinates.

<button className='locate' onClick={() => {
          setAddress('')
          navigator.geolocation.getCurrentPosition((position) => {
            panTo({
              lat: position.coords.latitude,
              lng: position.coords.longitude,
            })
            setCenter({
              lat: position.coords.latitude,
              lng: position.coords.longitude,
            })
          }, () => null);
        }}>Locate</button>
Enter fullscreen mode Exit fullscreen mode

Then I created the map itself. Inside the map I mapped through the different coordinates that had been converted from our database, and I displayed a marker at each place. I also included an info window that displays the information of each place.

<GoogleMap
          zoom={10}
          center={{lat: center.lat, lng: center.lng}}
          mapContainerClassName='map-container'
          options={options}
          onLoad={onMapLoad}
          // onBoundsChanged={onCenterChanged}
        >
          {coords.map(coord => {
            return(
              <Marker
                key={coord.lat}
                position={{ lat: parseFloat(coord.lat), lng: parseFloat(coord.lng) }}
                onClick={() => {
                  onCenterChanged()
                  setSelected(coord);
                }}
              />
            )
          })}
          {selected ? (
            <InfoWindow
              position={{ lat: selected.lat, lng: selected.lng }}
              onCloseClick={() => {
                setSelected(null);
              }}
            >
              <div>
                <h2>
                  {selected.title}
                </h2>
                <p>{selected.address}</p>
              </div>
            </InfoWindow>
          ) : null
          }



        </GoogleMap>
Enter fullscreen mode Exit fullscreen mode

Finally I added the places autocomplete searchbox. I also loaded the google maps places api through the script tag.

        <PlacesAutocomplete
          value={address}
          onChange={setAddress}
          onSelect={handleSelect}
        >
          {({ getInputProps, suggestions, getSuggestionItemProps }) => (
            <div>
              <input {...getInputProps({ placeholder: "Type address" })} />

              <div>
                {suggestions.map(suggestion => {
                  const style = {
                    backgroundColor: suggestion.active ? "#41b6e6" : "#fff"
                  };

                  return (
                    <div {...getSuggestionItemProps(suggestion, { style })}>
                      {suggestion.description}
                    </div>
                  );
                })}
              </div>
            </div>
          )}
        </PlacesAutocomplete>
        <Script
          src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBMePTwqFO2xPCaxUYqq0Vq4JQc631jo0o&libraries=places"
          strategy="beforeInteractive"
        ></Script>
Enter fullscreen mode Exit fullscreen mode

That is pretty much it. Keep in mind that this code is far from perfect. Also this code has literally zero styling so it is very ugly. It works though which is pretty cool. All in all this is the final code.

//maps.js

import React, {useState, useRef} from 'react';
import {GoogleMap, useLoadScript, Marker, InfoWindow,} from "@react-google-maps/api";
import PlacesAutocomplete, {geocodeByAddress, getLatLng} from 'react-places-autocomplete'

import Script from "next/script";
import prisma from "../lib/prisma";

const libraries = ['places']

export const getServerSideProps = async () => {
  const locations = await prisma.location.findMany();
  return { props: { locations } };
}

const App = ({ locations }) => {

  const [center, setCenter] = useState({
    lat: 0,
    lng: 0,
  });

  const [address, setAddress] = useState("");

  const [coords, setCoords] = useState([]);

  const [mapRef, setMapRef] = useState(null);

  const [selected, setSelected] = useState(null);

  const mapRef2 = useRef();

  const options = {
    disableDefaultUI: true,
    zoomControl: true,
  }

  const { isLoaded } = useLoadScript({
    googleMapsApiKey: process.env.NEXT_PUBLIC_MAPS_API_KEY,
    libraries,
  })

  const handleSelect = async (value) => {
    const results = await geocodeByAddress(value);
    const latLng = await getLatLng(results[0]);
    setAddress(value);
    setCenter(latLng);
  };

  const convertAddress = async (value) => {
    const results = await geocodeByAddress(value.address);
    const latLng = await getLatLng(results[0]);
    const locationData = {
      title: value.title,
      address: value.address,
      website: value.website,
      phone: value.phone,
      lat: latLng.lat,
      lng: latLng.lng
    }
    setCoords(coords => [...coords, locationData])
  };

  const onCenterChanged = () => {
    if (mapRef) {
      const newCenter = mapRef.getCenter();
      console.log(newCenter);
      setCenter({
        lat: mapRef.getCenter().lat(),
        lng: mapRef.getCenter().lng()
      })
    }
  }



  const onMapLoad = (map) => {
    mapRef2.current = map
    setMapRef(map);
    {locations.map(location => {
      convertAddress(location)
    })}
  }

  const panTo = React.useCallback(({lat, lng}) => {
    mapRef2.current.panTo({lat, lng});
  }, [])

  if (!isLoaded) {
    return (
      <div>
        <p>Loading...</p>
      </div>
    )
  }

  if (isLoaded) {
    return(
      <div>
        <button className='locate' onClick={() => {
          setAddress('')
          navigator.geolocation.getCurrentPosition((position) => {
            panTo({
              lat: position.coords.latitude,
              lng: position.coords.longitude,
            })
            setCenter({
              lat: position.coords.latitude,
              lng: position.coords.longitude,
            })
          }, () => null);
        }}>Locate</button>

        <GoogleMap
          zoom={10}
          center={{lat: center.lat, lng: center.lng}}
          mapContainerClassName='map-container'
          options={options}
          onLoad={onMapLoad}
          // onBoundsChanged={onCenterChanged}
        >
          {coords.map(coord => {
            return(
              <Marker
                key={coord.lat}
                position={{ lat: parseFloat(coord.lat), lng: parseFloat(coord.lng) }}
                onClick={() => {
                  onCenterChanged()
                  setSelected(coord);
                }}
              />
            )
          })}
          {selected ? (
            <InfoWindow
              position={{ lat: selected.lat, lng: selected.lng }}
              onCloseClick={() => {
                setSelected(null);
              }}
            >
              <div>
                <h2>
                  {selected.title}
                </h2>
                <p>{selected.address}</p>
              </div>
            </InfoWindow>
          ) : null
          }

        </GoogleMap>

        <PlacesAutocomplete
          value={address}
          onChange={setAddress}
          onSelect={handleSelect}
        >
          {({ getInputProps, suggestions, getSuggestionItemProps }) => (
            <div>
              <input {...getInputProps({ placeholder: "Type address" })} />

              <div>
                {suggestions.map(suggestion => {
                  const style = {
                    backgroundColor: suggestion.active ? "#41b6e6" : "#fff"
                  };

                  return (
                    <div {...getSuggestionItemProps(suggestion, { style })}>
                      {suggestion.description}
                    </div>
                  );
                })}
              </div>
            </div>
          )}
        </PlacesAutocomplete>



        <Script
          src="https://maps.googleapis.com/maps/api/js?key=AIzaSyBMePTwqFO2xPCaxUYqq0Vq4JQc631jo0o&libraries=places"
          strategy="beforeInteractive"
        ></Script>
      </div>
    )
  }
}


export default App;
Enter fullscreen mode Exit fullscreen mode

Also there is an error on line 168 because I didn't include a key. It is not breaking but you can just add a key to solve it.

Booh yah.

Oldest comments (1)

Collapse
 
ghostclonelol2000 profile image
<}:-{~ .A.K.a. DOOM

Hammer physics engine
Zay