DEV Community

Cover image for Make A Custom Map App with Redwood
Milecia
Milecia

Posted on • Originally published at mediajams.dev

Make A Custom Map App with Redwood

Having locations marked on a map is something that a lot of us depend on. Whether it's figuring out where a restaurant is or trying to find the nearest metro, we use maps quite a bit. Knowing how to work with maps as a developer is a niche skill with plenty of demand.

In this tutorial, we'll make a map app that lets users choose the location for the pin. It will save this location and upload a snapshot of the map to Cloudinary.

Some initial things

There are a couple of tools we need to set up before we start writing any code. We'll be using a local PostgreSQL database. If you don't have a local instance installed, you can download it for free here.

You'll also need a Cloudinary account to host your map snapshots. You can sign up for a free one here. Since we'll be uploading images from the front-end directly to Cloudinary, we need to set up an upload preset in our account settings. On this page, click the "Add upload preset" button.

add upload preset button

This will bring us to a new page where we can define specific values for our preset. There are only a couple of things we'll change here.

Update the "Signing Mode" to "Unsigned". This is how we'll handle the uploads from our client app. Then add a "Folder" value of "map-shots". This will put all of our uploaded map snapshots into one folder. Your settings should look similar to this.

upload preset setting value

Take a quick note of the "Upload preset name". You'll need this when it's time to upload images from the user. Now we can write some code.

Creating the app

We're going to make this app using Redwood. Open a terminal and run the following command.

yarn create redwood-app map-shot
Enter fullscreen mode Exit fullscreen mode

This creates a new Redwood app that has a fully functioning React front-end and GraphQL back-end. The front-end code is stored in the web directory and the back-end code is in the api directory. These are the two main folders we'll be working with.

Let's start by setting up a quick environment variable. You should have a .env file at the root of your project. Open that and add the connection string for your local Postgres instance. That might look something like this.

DATABASE_URL=postgres://admin:postgres@localhost:5432/map_shots
Enter fullscreen mode Exit fullscreen mode

Now we can start writing the schema for our database.

Defining the database schema

In api > db, open the schema.prisma file. This is where we define our database connection and table schema using Prisma. We first need to update the provider value to postgresql from sqlite. We set the DATABASE_URL value in the .env file so that's ready for us.

Now we can delete the example model and replace it with a schema for our map snapshots. Add the following code to replace the example model.

model Snapshot {
  id        String @id @default(uuid())
  name      String
  xPosition Float
  yPosition Float
  imageUrl  String
}
Enter fullscreen mode Exit fullscreen mode

These are the values that will be taken from the user and stored in our database. This is the only table we'll have for now so we can save these changes and run a database migration to persist them to Postgres. To do that, open your terminal and run:

yarn rw prisma migrate dev
Enter fullscreen mode Exit fullscreen mode

This will create the new database and this table we've defined. With all of this in place, we're done with the database work. Let's turn our attention to the GraphQL server.

Generating the GraphQL server

One of the best things about working with Redwood is how much work it does for you. We need to define the types and resolvers to make updates to the rows in our database. Normally we would make those files and write the code ourselves, but there's a command in Redwood that does this for us based on the schema we just wrote.

In your terminal, run the following command:

yarn rw g sdl snapshot --crud
Enter fullscreen mode Exit fullscreen mode

Now take a look in the api > src directory. You'll see some updates to the graphql and services directories. The command we just ran generated both the GraphQL types and all of the CRUD mutations and queries for us.

Take a look at api > src > graphql > snapshots.sdl.js to see the types. Then look through api > src > services > snapshots to see the queries, mutations, and test files that have been created for us.

We just created our entire GraphQL server with one command. You can modify any of the code in these files if you ever need more complex types or resolvers, but this gives you a way to quickly set up the basics.

Since we have the back-end ready to go, let's switch over to the front-end and set things up for our users to interact with.

Making the user interface

We'll need to add three packages in order to make our map. In a terminal, go to the web directory and run the following:

yarn add leaflet react-leaflet html-to-image
Enter fullscreen mode Exit fullscreen mode

This will give us access to react-leaflet which lets us do a lot of cool things with maps.

We need to use leaflet's stylesheet so that the map components render correctly. That means we need to add this code to index.html inside the <head>.

<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
crossorigin="" />
Enter fullscreen mode Exit fullscreen mode

With this package installed and configured, we can start working on the page for this map.

Add a new page

We'll use another Redwood command to generate the page and route for where the map will be rendered. In a terminal go to the root of your project and run this:

yarn rw g page map
Enter fullscreen mode Exit fullscreen mode

If you go to web > src > pages, you'll see a newly created folder called MapPage. This has several files to help enforce coding best practices, but the main one we'll work with is MapPage.js.

Add the core functionality to the page

We can delete all of the imports and clear out the code in the component to start fresh. Then we'll add the following packages we'll be using.

import { useMutation } from '@redwoodjs/web'
import { useCallback, useRef, useState } from 'react'
import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet'
import { toPng } from 'html-to-image'
Enter fullscreen mode Exit fullscreen mode

These are all of the methods and components we need to make this map app stored values in the database and images in Cloudinary.

Next we'll define the GraphQL mutation we need to add a new snapshot to the database. Right below the imports, add the following code:

const CREATE_SNAPSHOT_MUTATION = gql`
  mutation CreateSnapshotMutation($input: CreateSnapshotInput!) {
    createSnapshot(input: $input) {
      id
    }
  }
`
Enter fullscreen mode Exit fullscreen mode

Now we need to start adding functionality inside the MapPage component. We'll start by setting up some variables. Your component should look like this:

const MapPage = () => {
  const mapRef = useRef(null)
  const [createSnapshot] = useMutation(CREATE_SNAPSHOT_MUTATION)
  const [position, setPosition] = useState([36.49, -100])
  const [name, setName] = useState('The pit of the Midwest')
}
Enter fullscreen mode Exit fullscreen mode

We can write one of the functions that will let us handle our user changes. First, we'll write the function that will get called when a user submits the form we create a bit later. This will handle setting some states and it will call the method that uploads the image and adds the record to the database.

Below the initial variables, add the following function:

const updatePin = (e) => {
  e.preventDefault()

  const { xPosition, yPosition, name } = e.target.elements

  setPosition([xPosition.value, yPosition.value])
  setName(name.value)

  onCapture()
}
Enter fullscreen mode Exit fullscreen mode

This is going to handle the event from our form submission so that the page doesn't refresh. It gets the form values from the inputs and then updates the respective states. Then it calls the onCapture method, which is where some interesting things happen.

Save a snapshot of the map to Cloudinary and the database

Let's go ahead and make this onCapture method. Add the following code to your component:

const onCapture = useCallback(async () => {
  if (mapRef.current === null) {
    return
  }

  const dataUrl = await toPng(mapRef.current, { cacheBust: true })

  const uploadApi = `https://api.cloudinary.com/v1_1/${cloudName}/image/upload`

  const formData = new FormData()
  formData.append('file', dataUrl)
  formData.append('upload_preset', uploadPresetName)

  const res = await fetch(uploadApi, {
    method: 'POST',
    body: formData,
  })

  const result = await res.json()

  const input = {
    name: name,
    xPosition: Number(position[0]),
    yPosition: Number(position[1]),
    imageUrl: result.url,
  }

  createSnapshot({
    variables: { input },
  })
}, [mapRef])
Enter fullscreen mode Exit fullscreen mode

Quite a few things are happening here. First, we're checking to make sure our referenced component is loaded. Then we're using the toPng method from html-to-image to create a png data URL based on the elements inside our mapRef.

Next, add the API string for Cloudinary. This is where you'll need to use the cloud name from your account. Then we create a new FormData variable and add the data URL for the snapshot and the upload preset we created earlier.

Then we do a POST request to the API and upload the snapshot. We wait for the response to come back because it will have a Cloudinary URL we can use as the source for images in HTML elements. This is what we'll be saving to the database.

The last thing we do is make an input variable for our GraphQL call and then add this record to the database. All that's left is to write the elements that need to show to the user.

Adding the elements to the page

We need a return statement to render these elements. So add this code to your file and we'll talk through what's happening.

return (
  <div style={{ display: 'flex' }}>
    <div ref={mapRef}>
      <h1>{name}</h1>
      <MapContainer
        center={position}
        zoom={7}
        scrollWheelZoom={false}
        style={{ height: '550px', width: '750px' }}
      >
        <TileLayer
          attribution='&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors'
          url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
        />
        <Marker position={position}>
          <Popup>The armpit of Oklahoma.</Popup>.
        </Marker>
      </MapContainer>
    </div>
    <form onSubmit={updatePin}>
      <div>
        <label for="name">Change map name</label>
        <input type="text" name="name" />
      </div>
      <div>
        <label for="xPosition">Change pin x-position</label>
        <input type="number" name="xPosition" />
      </div>
      <div>
        <label for="yPosition">Change pin y-position</label>
        <input type="number" name="yPosition" />
      </div>
      <button type="submit">Update</button>
    </form>
  </div>
)
Enter fullscreen mode Exit fullscreen mode

We have a containing element with a flex style applied. This will show the map on the left of the screen and the form to its right. Then we'll have the container with our ref prop to capture the screenshot.

Inside of this, we have a title for the map and the map itself. The <MapContainer> component has a few props defined that tell the map where to be centered when a user first views the page.

In the <MapContainer> component, the <TileLayer> component is used to make up the map sections. You can check out the react-leaflet documentation for more information. The <Marker> component lets us set the little pinpoint you see on most maps.

Lastly, we have the <form> that calls the updatePin method whenever it is submitted. This is what triggers all of the functionality we wrote out earlier.

It's time to run the app and see all of this in action. At the root of your project, run this in the terminal:

yarn rw dev
Enter fullscreen mode Exit fullscreen mode

This starts up the local development server for both the GraphQL server and the front-end. When the app opens in the browser, you should see something that looks like this:

a map with a pin in the armpit of Oklahoma

Now you can update the map with the fields to the right. Keep in mind that the map won't auto-focus on the new point, so you might need to zoom out to see where you land. Here's an example of an update.

moved the pin to New York instead of Oklahoma

If you take a look at your Postgres instance, you'll see new records in your table and if you look on Cloudinary you'll see a new "map-shots" folder.

That's it! We're finally finished with the project.

Finished code

If you want to take a look at the complete code, check out the map-shot folder of this repo.

You can check out the front-end in this Code Sandbox.

Conclusion

You can expand this app to do many interesting things like making paths to special locations or pointing out obscure landmarks. One of the tricky things is making sure you stay on land! Hopefully, you see how you can bring together a lot of different technologies quickly to make a sophisticated app.

Top comments (0)