Make A Custom Map App with Redwood
Published: 2021-11-30 14:31:48

---
title: Make A Custom Map App with Redwood
published: true
description: Working with map data can lead to some interesting projects. Whether you want to pinpoint locations for users to visit or you want to show where equipment is located for a manufacturing company, maps can lead to unexpected insights. In this tutorial, we'll take a look some basics of working with a map.
tags: 
cover_image: https://cdn.sanity.io/images/5ad74sb4/production/422ebf1eb4ee327d7d788bcb826d61c87b983855-1000x420.png?w=2000&h=2000&fit=max
canonical_url: https://mediajams.dev/post/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](https://www.postgresql.org/download/).

You'll also need a Cloudinary account to host your map snapshots. You can [sign up for a free one here](https://cloudinary.com/users/register/free). Since we'll be uploading images from the front-end directly to Cloudinary, we need to [set up an upload preset](https://cloudinary.com/console/settings/upload) in our account settings. On this page, click the "Add upload preset" button.

![add upload preset button](https://res.cloudinary.com/jesse-thisdot/image/upload/v1636137620/e-603fc55d218a650069f5228b/s76i6knunxmxxdhiuy6v.png)

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](https://res.cloudinary.com/jesse-thisdot/image/upload/v1636137647/e-603fc55d218a650069f5228b/efz4ykalgxzh50uciz1c.png)

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](https://redwoodjs.com/). Open a terminal and run the following command.

```bash
yarn create redwood-app map-shot
```

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.

```env
DATABASE_URL=postgres://admin:postgres@localhost:5432/map_shots
```

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](https://www.prisma.io/). 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.

```javascript
model Snapshot {
  id        String @id @default(uuid())
  name      String
  xPosition Float
  yPosition Float
  imageUrl  String
}
```

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:

```bash
yarn rw prisma migrate dev
```

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:

```bash
yarn rw g sdl snapshot --crud
```

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:

```bash
yarn add leaflet react-leaflet html-to-image
```

This will give us access to [react-leaflet](https://react-leaflet.js.org/) 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>`.

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

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:

```bash
yarn rw g page map
```

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.

```javascript
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'
```

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:

```javascript
const CREATE_SNAPSHOT_MUTATION = gql`
  mutation CreateSnapshotMutation($input: CreateSnapshotInput!) {
    createSnapshot(input: $input) {
      id
    }
  }
`
```



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:

```javascript
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')
}
```

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:

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

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

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

  onCapture()
}
```

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:

```javascript
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])
```


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.

```javascript
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>
)
```

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:

```bash
yarn rw dev
```

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](https://res.cloudinary.com/jesse-thisdot/image/upload/v1636137750/e-603fc55d218a650069f5228b/oohnxnvisfslimqwynd0.png)

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](https://res.cloudinary.com/jesse-thisdot/image/upload/v1636137763/e-603fc55d218a650069f5228b/g6fuaenb87s8hapz9wtx.png)

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](https://github.com/flippedcoder/blog-examples/tree/main/map-shot).

You can check out the front-end in [this Code Sandbox](https://codesandbox.io/s/eager-bassi-yz0hf).

## 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.