Building a Fullstack Road trip mapper app using the absolute power of MERN stack šŸ”„

pramit_marattha

Pramit Marattha

Posted on December 10, 2021

Building a Fullstack Road trip mapper app using the absolute power of MERN stack šŸ”„

This article concentrates on the most critical tasks and concepts for better understanding and building MERN stack applications from the ground up. It's for folks who are serious about learning about the MERN stack and want to concentrate on the essentials. We'll build a full-stack road trip mapper application where users can pin and map locations and view the sites pinned by other users, all using the MERN stack and leveraging the power of the Mapbox API. This blog session will teach you the fundamentals of MERN stack technology as well as advanced concepts and operations.

Here's a quick preview of our application's final version:

Demo

Demo

Demo

Demo

There is a separate article where you may learn about the MERN stack in very great detail.

https://aviyel.com/post/1278

Setting up the folder structure

Create two folders inside your project directory called client and server, then open them in Visual Studio Code or any other code editor of your choice.

Making Directory

Folder structure

Now, we'll create a MongoDB database, a Node and Express server, a database schema to represent our project case study application, and API routes to create, read, update, and delete data and information from the database using npm and the appropriate packages. So, open a command prompt, navigate to your server's directory, and then run the code below.

npm init -yes
Enter fullscreen mode Exit fullscreen mode

Configuring package.json file

Execute the following commands in the terminal to install the dependencies.

npm install cors dotenv express express-rate-limit mongoose nodemon body-parser helmet morgan rate-limit-mongo
Enter fullscreen mode Exit fullscreen mode

Dependencies

  • Dotenv: Dotenv is a zero-dependency module that loads environment variables from a .env file into process.env

  • cors: This module allows to relax the security applied to an API

  • express: Fast, unopinionated, minimalist web framework for node.

  • express-rate-limit: Basic IP rate-limiting middleware for Express. It is used to limit repeated requests to public APIs and/or endpoints such as password reset.

  • mongoose: It is an Object Data Modeling library for MongoDB and Node. js

  • nodemon: This module helps to develop node.js based applications by automatically restarting the application when file changes in the directory are detected.

  • body-parser: Node.js body parsing middleware.

  • helmet: Helmet.js fills in the gap between Node.js and Express.js by securing HTTP headers that are returned by Express applications.

  • morgan : HTTP request logger middleware for node.js

  • rate-limit-mongo : MongoDB store for the express-rate-limit middleware.

Dependencies Installation

The "package.json" file should look like this after the dependencies have been installed.

Package JSON

And also, remember to update the scripts as well.

Scripts

Now go to your server directory, create a src folder, and an index.js file there.

Setting up index.js

  • Import express module.

  • Import and configure dotenv module

  • Import helmet module.

  • Import morgan module.

  • Import CORS module

  • Use express() to initialize our app.

//src/index.js
const express = require('express');
// NOTE morgan is a logger
const morgan = require('morgan');
const helmet = require('helmet');
const cors = require('cors');
const mongoose = require('mongoose');

require('dotenv').config();

// app config
const app = express();
Enter fullscreen mode Exit fullscreen mode

We may now utilize all of the other methods on that app instance. Let's start with the fundamentals and very basic setups. Don't forget to set up the port and cors, too.

const express = require('express');
// NOTE morgan is a logger
const morgan = require('morgan');
const helmet = require('helmet');
const cors = require('cors');
const mongoose = require('mongoose');

require('dotenv').config();

const app = express();

const port = process.env.PORT || 4000;

app.use(morgan('common'));
app.use(helmet());
app.use(cors({
  origin: process.env.CORS_ORIGIN,
}));

app.use(express.json());

app.get('/', (req, res) => {
  res.json({
    message: 'Hello There',
  });
});
Enter fullscreen mode Exit fullscreen mode

Now it's time to connect our server application to a real database. Here we'll use the MongoDB database, specifically the MongoDB cloud Atlas version, which means our database will be hosted on their cloud.

Setting up MongoDB Atlas cloud cluster

MongoDB is a document-oriented database that is open-source and cross-platform. MongoDB is a NoSQL database that stores data in JSON-like documents and has optional schemas. All versions were given under the AGPL license prior to October 16, 2018. All versions released after October 16, 2018, including bug fixes for prior versions, are covered by the SSPL license v1. You can also learn more about MongoDB setup and configuration from the following article.

https://aviyel.com/post/1323

To set up and start your MongoDB cluster, follow the exact same steps mentioned below.

Official MongoDB website*
MongoDB atlas started

Sign-up MongoDB
MongoDB atlas Sign-up

Sign-in to MongoDB
MongoDb Atlas Login

Create a Project
Creating a Project

Adding members
Adding a Members

Building Database
Building a Database

Creating a Cluster
Creating a Cluster

Selecting a cloud service Provider
Choosing a service provider

Configuring Security
Security Configurations

Database Deployment to the Cloud
Database Deployment to the cloud

Navigate to the network access tab and select "Add IP address."
Add IP address

Now, select the Choose a connection method.
Connection Method

Connecting to cluster
Connecting to the cluster

Create a new variable called DATABASE_CONNECTION inside index.js . Create a string and paste the copied mongo DB connection URL into it. Now, inside it, type your username and password, removing any brackets and entering your own credentials. We'll create environmental variables to safeguard the credential later, but for now, let's add it this way. The second thing we'll need is a PORT, so just type in 4000 for now. Finally, we'll use mongoose to connect to our database, so type in mongoose. connect(), which is a function with two parameters. The DATABASE_CONNECTION will be the first, and the object with two choices will be the second. The first is useNewUrlParser, which we'll enable, and the second is useUnifiedTopology, which we'll enable as well. These objects are optional, but we will see some errors or warnings on our console. Let's chain it with .then() and .catch() inside,then() function. This will simply call the app and invoke listen, leading to two parameters: PORT and the callback function that will be executed if our application is successfully connected to the database. Finally, if the connection to the database is unsuccessful, we will simply console log our error message. Your index.js file should now look something like this.

//src/index.js
const express = require('express');
// NOTE morgan is a logger
const morgan = require('morgan');
const helmet = require('helmet');
const cors = require('cors');
const mongoose = require('mongoose');

require('dotenv').config();

const app = express();

const DATABASE_CONNECTION = process.env.DATABASE_URL;

mongoose.connect(DATABASE_CONNECTION, {
  useNewUrlParser: true,
  newUnifiedTopology: true,
});

app.use(morgan('common'));
app.use(helmet());
app.use(cors({
  origin: process.env.CORS_ORIGIN,
}));

app.use(express.json());

app.get('/', (req, res) => {
  res.json({
    message: 'Hello There',
  });
});

const port = process.env.PORT || 4000;
app.listen(port, () => {
  console.log(`Currently Listening at http://localhost:${port}`);
});
Enter fullscreen mode Exit fullscreen mode

Insert mongodb+srv into the .env file.

PORT=4000
DATABASE_URL=mongodb+srv://pramit:<password>@cluster0.8tw83.mongodb.net/myFirstDatabase?retryWrites=true&w=majority
CORS_ORIGIN=http://localhost:3000
Enter fullscreen mode Exit fullscreen mode

We now have successfully connected our server to the database, let's create middleware first before we get started on building our backend application's routes and database schema. To do so, we'll need to create a new file called middlewares.js and within that file, we will create a two function called notFound and errorHandler

and export those functions. So let create notFound middleware so typically this middleware should be the last middleware that is registered so this middleware takes in req, res, and next. Basically, if a request ever made it here, it means we didn't locate the route users were searching for, so we'll create a variable and send them a message, and then we'll pass that to our next middleware, which is errorHander Middleware but before that don't forget to pass the response status of 404 as well. Now let's make our errorHandler middleware, which has four parameters instead of three, so we'll have (error,req, res, next). The first thing we'll do is set a status code and check whether it's 200 or use the status code that's already been specified, and then we'll simply set the status code, and then we'll respond with some JSON that will display the error message.

//middlewares.js
const notFound = (req, res, next) => {
  const error = new Error(`Not Found - ${req.originalUrl}`);
  res.status(404);
  next(error);
};

const errorHandler = (error, req, res, next) => {
  const statusCode = res.statusCode === 200 ? 500 : res.statusCode;
  res.status(statusCode);
  res.json({
    message: error.message,
    stack: process.env.NODE_ENV === "production" ? "nope" : error.stack,
  });
};

module.exports = {
  notFound,
  errorHandler,
};
Enter fullscreen mode Exit fullscreen mode

So, after modifying the middlewares.js file, import and use the middleware as needed in the index.js file.

//src/index.js
const express = require("express");
// NOTE morgan is a logger
const morgan = require("morgan");
const helmet = require("helmet");
const cors = require("cors");
const mongoose = require("mongoose");

require("dotenv").config();

const middlewares = require("./middlewares");
const app = express();

const DATABASE_CONNECTION = process.env.DATABASE_URL;

mongoose.connect(DATABASE_CONNECTION, {
  useNewUrlParser: true,
  newUnifiedTopology: true,
});

app.use(morgan("common"));
app.use(helmet());
app.use(
  cors({
    origin: process.env.CORS_ORIGIN,
  })
);

app.use(express.json());

app.get("/", (req, res) => {
  res.json({
    message: "Hello There",
  });
});

app.use(middlewares.notFound);
app.use(middlewares.errorHandler);

const port = process.env.PORT || 4000;
app.listen(port, () => {
  console.log(`Currently Listening at http://localhost:${port}`);
});
Enter fullscreen mode Exit fullscreen mode

let's create a LogEntry model. Create a folder called models and inside it, create one file called LogEntry.model.js and within that following file structure your DB schema by defining title, description, comments, image, ratings, latitude and longitude as shown below.

//models/LogEntry.model.js
const mongoose = require("mongoose");
const { Schema } = mongoose;

const requiredNumber = {
  type: Number,
  required: true,
};

const logEntrySchema = new Schema(
  {
    title: {
      type: String,
      required: true,
    },
    description: String,
    comments: String,
    image: String,
    rating: {
      type: Number,
      min: 0,
      max: 10,
      default: 0,
    },
    latitude: {
      ...requiredNumber,
      min: -90,
      max: 90,
    },
    longitude: {
      ...requiredNumber,
      min: -180,
      max: 180,
    },
    visitDate: {
      required: true,
      type: Date,
    },
  },
  {
    timestamps: true,
  }
);

const LogEntry = mongoose.model("collections", logEntrySchema);

module.exports = LogEntry;
Enter fullscreen mode Exit fullscreen mode

The structure of your files and folders should now look like this.

Folder Structure

Now that we've successfully created our DB Schema, let's get started on creating our routes for our backend application. To do so, we'll need to create a new folder inside the src directory and name it as routes Within the routes folder, we will create a js file called logs.routes.js.so first we must import express from "express" and also configure our router and import our recently created DB schema. Now we can begin adding our routes to it.

Folder Structure

const { Router } = require("express");

const LogEntry = require("../models/LogEntry.model.js");

const { API_KEY } = process.env;

const router = Router();
Enter fullscreen mode Exit fullscreen mode

fetches all the pinned location information.

router.get("/", async (req, res, next) => {
  try {
    const entries = await LogEntry.find();
    res.json(entries);
  } catch (error) {
    next(error);
  }
});
Enter fullscreen mode Exit fullscreen mode

Insert/add a pinned location with authorized access

router.post("/", async (req, res, next) => {
  try {
    if (req.get("X-API-KEY") !== API_KEY) {
      res.status(401);
      throw new Error("Unauthorized Access");
    }
    const logEntry = new LogEntry(req.body);
    const createdEntry = await logEntry.save();
    res.json(createdEntry);
  } catch (error) {
    if (error.name === "ValidationError") {
      res.status(422);
    }
    next(error);
  }
});
Enter fullscreen mode Exit fullscreen mode

exporting the router

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

Your logs.routes.js should resemble something like this

//src/routes/logs.routes.js
const { Router } = require("express");

const LogEntry = require("../models/LogEntry.model.js");

const { API_KEY } = process.env;

const router = Router();

router.get("/", async (req, res, next) => {
  try {
    const entries = await LogEntry.find();
    res.json(entries);
  } catch (error) {
    next(error);
  }
});

router.post("/", async (req, res, next) => {
  try {
    if (req.get("X-API-KEY") !== API_KEY) {
      res.status(401);
      throw new Error("Unauthorized Access");
    }
    const logEntry = new LogEntry(req.body);
    const createdEntry = await logEntry.save();
    res.json(createdEntry);
  } catch (error) {
    if (error.name === "ValidationError") {
      res.status(422);
    }
    next(error);
  }
});

module.exports = router;
Enter fullscreen mode Exit fullscreen mode

Now, update your .env file

NODE_ENV=production
PORT=4000
DATABASE_URL=mongodb+srv://pramit:<password>@cluster0.8tw83.mongodb.net/myFirstDatabase?retryWrites=true&w=majority
CORS_ORIGIN=http://localhost:3000
API_KEY=roadtripmapper
Enter fullscreen mode Exit fullscreen mode

Let's get started by importing the logs routes into your index.js file. We can now connect map pinned log info to our application using express middleware. Finally, your root index.js file should be like the following.

//src/index.js
const express = require("express");
// NOTE morgan is a logger
const morgan = require("morgan");
const helmet = require("helmet");
const cors = require("cors");
const mongoose = require("mongoose");

require("dotenv").config();

const middlewares = require("./middlewares");
const logs = require("./routes/logs.routes.js");
const app = express();

const DATABASE_CONNECTION = process.env.DATABASE_URL;

mongoose.connect(DATABASE_CONNECTION, {
  useNewUrlParser: true,
  newUnifiedTopology: true,
});

app.use(morgan("common"));
app.use(helmet());
app.use(
  cors({
    origin: process.env.CORS_ORIGIN,
  })
);

app.use(express.json());

app.get("/", (req, res) => {
  res.json({
    message: "Hello There",
  });
});

app.use("/api/logs", logs);

app.use(middlewares.notFound);
app.use(middlewares.errorHandler);

const port = process.env.PORT || 4000;
app.listen(port, () => {
  console.log(`Currently Listening at http://localhost:${port}`);
});
Enter fullscreen mode Exit fullscreen mode

After restarting the server, you should see something like this:

Server Running


Setting up the frontend with react

In the next step let's start with the frontend and build it with react. The first thing you need to do is install Node.js if it isn't already installed on your machine. So, go to the Node.js official website and download the most recent version. You'll require Node js to utilize the node package manager, generally known as NPM. Now navigate to the client folder in your favorite code editor. Visual Studio Code will be my tool of choice. Then, in the integrated terminal, type npx create-react-app. This command will create a client application in the current directory with the name client.

create-react-app

ThereĀ isĀ aĀ separateĀ articleĀ whereĀ youĀ mayĀ learnĀ everythingĀ thereĀ isĀ toĀ knowĀ about

cleaning up boilerplate react project.

https://aviyel.com/post/1190

It's time to install some packages within react-boilerplate now that you've installed and cleaned it. so copy and paste the following command into your terminal.

npm i react-hook-form react-map-gl react-rating-stars-component react-responsive-animate-navbar
Enter fullscreen mode Exit fullscreen mode

Dependencies

  • react-hook-form: Performant, flexible, and extensible forms library for React Hooks.

  • react-map-gl: react-map-gl is a suite of React components designed to provide a React API for Mapbox GL JS-compatible libraries

  • react-rating-stars-component: Simple star rating component for your React projects.

  • react-responsive-animate-navbar : simple, flexible & completely customizable responsive navigation bar component.

Dependencies Installation

After installing all these packages your packge.json file of the client should look like this:

Package json

Let's construct two separate folders /components inside the components folder after we've installed all of our project's dependencies and name it as RoadTripNav and TripEntryForm .

Your file and folder structure should look something like this once you've added all of your components.

Folder Structure

Now that you have all of the project's components set up, it's time to start coding. First, import the ReactNavbar from "react-responsive-animate-navbar" and customize the color of your navbar, add the logo to the public folder and import it directly, and donā€™t forget to add some social links as well. The following is an example of how the code should appear.

RoadTripNavComponent

// components/RoadTripNav
import React from "react";
import * as ReactNavbar from "react-responsive-animate-navbar";
// import roadTripSvg from "../../assets/roadtrip.svg";

const RoadTripNav = () => {
  return (
    <ReactNavbar.ReactNavbar
      color="rgb(25, 25, 25)"
      logo="./logo.svg"
      menu={[]}
      social={[
        {
          name: "Twitter",
          url: "https://twitter.com/pramit_armpit",
          icon: ["fab", "twitter"],
        },
      ]}
    />
  );
};

export default RoadTripNav;
Enter fullscreen mode Exit fullscreen mode

Before we go any further, let's set up our Mapbox. First, go to the Mapbox site and log in or sign up if you don't already have an account. Next, create your own custom map style in the Mapbox Studio and publish it. Finally, go back to the dashboard and copy the default public API key provided by MapBox.

Mapbox

Login or create your MapBox account

Mapbox login

Click on design a custom map style

Mapbox studio

Customize your own style of the map inside the Mapbox studio

MapBox studio Editor

Copy the default public token
Access Token

After you've successfully obtained your public token, go to the env file or create one if you donā€™t have and after that create a variable named as REACT_APP_MAPBOX_TOKEN, then paste that token into that variable. This is what your env file should look like.

REACT_APP_MAPBOX_TOKEN= ************************************ // add token
Enter fullscreen mode Exit fullscreen mode

Before we go any further, let's make an api and styles folder in our root source directory. Inside the api folder, make a API.js file, and inside the styles folder, make a index.css file where all our styles of the application will be added. This is how your folder structure should appear.

Folder Structure

Now go to the newly created API file and construct two functions called "listLogEntries" to collect all the log entries from the backend and "createLogEntries" to create or send the post request / post the entries to the backend, as well as export these functions. Also, don't forget to include the URL where your server is running.

API

//api/API.js
const API_URL = "http://localhost:4000";
// const API_URL = window.location.hostname === "localhost" ? "http://localhost:4000" : "https://road-trip-map-mern.herokuapp.com" ;

export async function listLogEntries() {
  const response = await fetch(`${API_URL}/api/logs`);
  // const json = await response.json();
  return response.json();
}

export async function createLogEntries(entry) {
  const api_key = entry.api_key;
  delete entry.api_key;
  const response = await fetch(`${API_URL}/api/logs`, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "X-API-KEY": api_key,
    },
    body: JSON.stringify(entry),
  });
  // const json = await response.json();
  // return response.json();
  let json;
  if (response.headers.get("content-type").includes("text/html")) {
    const message = await response.text();
    json = {
      message,
    };
  } else {
    json = await response.json();
  }
  if (response.ok) {
    return json;
  }
  const error = new Error(json.message);
  error.response = json;
  throw error;
}
Enter fullscreen mode Exit fullscreen mode

Let's make a submission form for the pinned map location. To do so, open the TripEntryForm component from the component folder we previously made, import the useForm hook from react-hook-form, import createLogentries from api, and then import the useState hook from the React library because this hook will enable us to integrate the state into our functional component. useState(), unlike state in class components, does not work with object values. If necessary, we can use primitives directly and create multiple react hooks for multiple variables. Now, create two states: loading and error, and then destructure register and handleSubmit from the useForm() hook from "react-hook-form" library.After you've completed that, it's time to craft our form, but first let's create a function to handle our submit request. To do so, create an asynchronous onSubmit function and inside it, simply create a try-catch block. Inside the try block set the loading to true, configure the latitude and longitude, console log the data, and invoke the onClose function, and finally inside the catch block, pass the error message to the error state, set the loading to false and simply console log the error message and then simply create a form inside the return statement exactly shown in the code below.

TripEntry Form

// components/TripEntryForm.js
import React, { useState } from "react";
import { useForm } from "react-hook-form";
import { createLogEntries } from "../../api/API";
import "./TripEntryForm.css";

const TripEntryForm = ({ location, onClose }) => {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState("");

  const { register, handleSubmit } = useForm();

  const onSubmit = async (data) => {
    try {
      setLoading(true);
      data.latitude = location.latitude;
      data.longitude = location.longitude;
      const created = await createLogEntries(data);
      console.log(created);
      onClose();
    } catch (error) {
      setError(error.message);
      console.error(error);
      setLoading(false);
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="trip-form">
      {error ? <h3 className="error-message">{error}</h3> : null}
      <label htmlFor="api_key">Enter Password</label>
      <input
        type="password"
        name="api_key"
        placeholder="For demo, password => {roadtripmap} "
        required
        ref={register}
      />

      <label htmlFor="title">Title</label>
      <input name="title" placeholder="Title" required ref={register} />

      <label htmlFor="comments">Comments</label>
      <textarea
        name="comments"
        placeholder="Comments"
        rows={3}
        ref={register}
      ></textarea>

      <label htmlFor="description">Description</label>
      <textarea
        name="description"
        placeholder="Describe your journey"
        rows={4}
        ref={register}
      ></textarea>

      <label htmlFor="image">Image</label>
      <input name="image" placeholder="Image URL" ref={register} />

      <label htmlFor="rating">Rating (1 - 10)</label>
      <input name="rating" type="number" min="0" max="10" ref={register} />

      <label htmlFor="visitDate">Visit Date</label>
      <input name="visitDate" type="date" required ref={register} />

      <button disabled={loading}>
        <span>{loading ? "Submitting..." : "Submit your Trip"}</span>
      </button>
    </form>
  );
};

export default TripEntryForm;
Enter fullscreen mode Exit fullscreen mode

Also, don't forget to add the TripEntryForm styles inside that very own component folder and name it as TripEntryForm.css and paste the exact CSS code as mentioned below

TripEntry Form style

//TripEntryForm.css
@import url("https://fonts.googleapis.com/css2?family=Fredoka+One&family=Poppins:ital,wght@0,200;0,400;1,200;1,300&family=Roboto:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap");

.trip-form label {
  margin: 0.5rem 0;
  display: block;
  width: 100%;
  color: rgb(255, 255, 255);
  font-family: "Fredoka One", cursive;
}
.trip-form input {
  margin: 0.5rem 0;
  background-color: #2c2e41;
  border-radius: 5px;
  border: 0;
  box-sizing: border-box;
  color: rgb(255, 255, 255);
  font-size: 12px;
  height: 100%;
  outline: 0;
  padding: 10px 5px 10px 5px;
  width: 100%;
  font-family: "Fredoka One", cursive;
}

.trip-form textarea {
  margin: 0.5rem 0;
  background-color: #2c2e41;
  border-radius: 5px;
  border: 0;
  box-sizing: border-box;
  color: rgb(255, 255, 255);
  font-size: 12px;
  height: 100%;
  outline: 0;
  padding: 10px 5px 10px 5px;
  width: 100%;
  font-family: "Fredoka One", cursive;
}

.error-message {
  color: red;
}

.trip-form button {
  background-color: #fb5666;
  border-radius: 12px;
  border: 0;
  box-sizing: border-box;
  color: #eee;
  cursor: pointer;
  font-size: 18px;
  height: 50px;
  margin-top: 38px;
  outline: 0;
  text-align: center;
  width: 100%;
}

button span {
  position: relative;
  z-index: 2;
}

button:after {
  position: absolute;
  content: "";
  top: 0;
  left: 0;
  width: 0;
  height: 100%;
  transition: all 2.35s;
}

button:hover {
  color: #fff;
}

button:hover:after {
  width: 100%;
}

.small_description {
  font-size: 60px;
}
Enter fullscreen mode Exit fullscreen mode

Now go to this repo and download all of the SVG files that are available there.

https://github.com/pramit-marattha/road-trip-mapper-mern-app/tree/main/client/src/assets

After you've downloaded all of the svg files, go to the main app component, and begin importing all of the key requirements from the libraries we previously installed, such as ReactMapGl, marker, and popup from the "react-map-gl" library, import all of the components as well as svgs from the assets folder, and finally create four state logEntries whose initial value is empty array, showPopup whose initial value is an empty object, addEntryLocation has a default value of null, and for viewport specify the initial value exactly like the code mentioned below or you can add whatever you want. Create an asynchronous function called getEntries that asynchronously calls the listLogEntries function that was previously established within the api file and whose main task is to retrieve all of the entries made by the users and feed them to the logEntries state and then call that function inside the useEffect() hook by using this Hook, you tell React that your component needs to do something after render.

React will remember the function you passed (weā€™ll refer to it as our ā€œeffectā€), and call it later after performing the DOM updates. To this effect, we set the document title, but we could also perform data fetching or call some other imperative API. Placing useEffect() inside the component lets us access the count state variable (or any props) right from the effect. We donā€™t need a special API to read it ā€” itā€™s already in the function scope. Hooks embrace JavaScript closures and avoid introducing React-specific APIs where JavaScript already provides a solution.useEffect() the hook is somewhat similar to the life-cycle methods that we are aware of for class components. It runs after every render of the component including the initial render. Hence it can be thought of as a combination of componentDidMount, componentDidUpdate, and componentWillUnmount.If we want to control the behavior of when the effect should run (only on initial render, or only when a particular state variable changes), we can pass in dependencies to the effect to do so. This hook also provides a clean-up option to allow cleaning up of resources before the component is destroyed. basic syntax of the effect: useEffect(didUpdate) .

Create a function named showMarkerPopup and provide the event parameters to it. Inside that function, destruct the latitude and longitude from "event.lngltd" and pass it to addEntryLocation state. Finally, employ all of the imported components within our return statement by simply following the code shown below.

App component

//src/app.js
import * as React from "react";
import { useState, useEffect } from "react";
import ReactMapGL, { Marker, Popup } from "react-map-gl";
import { listLogEntries } from "./api/API";
import MapPinLogo from "./assets/mapperPin.svg";
import MarkerPopup from "./assets/MarkerPopup.svg";
import TripEntryForm from "./components/TripEntryForm";
import ReactStars from "react-rating-stars-component";
import RoadTripNav from "./components/RoadTripNav/RoadTripNav";

const App = () => {
  const [logEntries, setLogEntries] = useState([]);
  const [showPopup, setShowPopup] = useState({});
  const [addEntryLocation, setAddEntryLocation] = useState(null);
  const [viewport, setViewport] = useState({
    width: "100vw",
    height: "100vh",
    latitude: 27.7577,
    longitude: 85.3231324,
    zoom: 7,
  });

  const getEntries = async () => {
    const logEntries = await listLogEntries();
    setLogEntries(logEntries);
    console.log(logEntries);
  };

  useEffect(() => {
    getEntries();
  }, []);

  const showMarkerPopup = (event) => {
    console.log(event.lngLat);
    const [longitude, latitude] = event.lngLat;
    setAddEntryLocation({
      longitude,
      latitude,
    });
  };

  return (
    <>
      <RoadTripNav />
      <ReactMapGL
        {...viewport}
        mapStyle="mapbox://styles/pramitmarattha/ckiovge5k3e7x17tcmydc42s3" 
        mapboxApiAccessToken={process.env.REACT_APP_MAPBOX_TOKEN}
        onViewportChange={(nextViewport) => setViewport(nextViewport)}
        onDblClick={showMarkerPopup}
      >
        {logEntries.map((entry) => (
          <React.Fragment key={entry._id}>
            <Marker latitude={entry.latitude} longitude={entry.longitude}>
              <div
                onClick={() =>
                  setShowPopup({
                    // ...showPopup,
                    [entry._id]: true,
                  })
                }
              >
                <img
                  className="map-pin"
                  style={{
                    width: `${5 * viewport.zoom}px`,
                    height: `${5 * viewport.zoom}px`,
                  }}
                  src={MapPinLogo}
                  alt="Map Pin Logo"
                />
              </div>
            </Marker>
            {showPopup[entry._id] ? (
              <Popup
                latitude={entry.latitude}
                longitude={entry.longitude}
                closeButton={true}
                closeOnClick={false}
                dynamicPosition={true}
                onClose={() => setShowPopup({})}
                anchor="top"
              >
                <div className="popup">
                  <ReactStars
                    count={10}
                    value={entry.rating}
                    size={29}
                    activeColor="#ffd700"
                  />
                  <div className="popup_image">
                    {entry.image && <img src={entry.image} alt={entry.title} />}
                  </div>
                  <h3>{entry.title}</h3>
                  <p>{entry.comments}</p>
                  <small>
                    Visited :{" "}
                    {new Date(entry.visitDate).toLocaleDateString("en-US", {
                      weekday: "long",
                      year: "numeric",
                      month: "long",
                      day: "numeric",
                    })}
                  </small>
                  <p>Ratings: {entry.rating}</p>
                  <div className="small_description">{entry.description}</div>
                </div>
              </Popup>
            ) : null}
          </React.Fragment>
        ))}
        {addEntryLocation ? (
          <>
            <Marker
              latitude={addEntryLocation.latitude}
              longitude={addEntryLocation.longitude}
            >
              <div>
                <img
                  className="map-pin"
                  style={{
                    width: `${8 * viewport.zoom}px`,
                    height: `${8 * viewport.zoom}px`,
                  }}
                  src={MarkerPopup}
                  alt="Map Pin Logo"
                />
              </div>
              {/* <div style={{color:"white"}}>{entry.title}</div> */}
            </Marker>

            <Popup
              latitude={addEntryLocation.latitude}
              longitude={addEntryLocation.longitude}
              closeButton={true}
              closeOnClick={false}
              dynamicPosition={true}
              onClose={() => setAddEntryLocation(null)}
              anchor="top"
            >
              <div className="popup">
                <TripEntryForm
                  onClose={() => {
                    setAddEntryLocation(null);
                    getEntries();
                  }}
                  location={addEntryLocation}
                />
              </div>
            </Popup>
          </>
        ) : null}
      </ReactMapGL>
    </>
  );
};

export default App;
Enter fullscreen mode Exit fullscreen mode

The very final step is to add all of the styles to our project, which can be done by going to our previously established styles folder and copying and pasting the following mentioned code into the index.css file.

index css

/* styles/index.css */
@import url("https://fonts.googleapis.com/css2?family=Fredoka+One&family=Poppins:ital,wght@0,200;0,400;1,200;1,300&family=Roboto:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap");

body {
  margin: 0;
  font-family: "Fredoka One", cursive;
  height: 100vh;
  width: 100vw;
  overflow: hidden;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
    monospace;
}

.map-pin {
  position: absolute;
  transform: translate(-50%, -100%);
  z-index: -1;
}

.popup {
  width: 20vw;
  height: auto;
  padding: 1rem;
  background-color: #8661d1;
  border-radius: 5px;
  z-index: 999;
}

.popup img {
  width: 40%;
  height: auto;
  border-radius: 5%;
  justify-content: center;
  align-items: center;
  margin: 0 auto;
  padding-top: 1rem;
}

.popup_image {
  display: flex;
  justify-content: center;
  align-items: center;
}

.small_description {
  font-size: 1.5rem;
  color: #fff;
  border-radius: 5px;
  z-index: 999;
}

button {
  border: none;
  color: #fa5252;
  padding-right: 1rem;
  border-radius: 50%;
  font-size: 4rem;
  margin-top: 0.2rem;
  height: auto;
  cursor: pointer;
}
Enter fullscreen mode Exit fullscreen mode

Finally, start both the client and the server.

React Running

Application up and running

Demo

This application's entire source code is available here.

https://github.com/aviyeldevrel/devrel-tutorial-projects/tree/main/MERN-roadtrip-mapper

Main article available here => https://aviyel.com/post/1430

Happy Coding!!

Follow @aviyelHQ or sign-up on Aviyel for early access if you are a project maintainer, contributor, or just an Open Source enthusiast.

Join Aviyel's Discord => Aviyel's world

Twitter =>[https://twitter.com/AviyelHq]

šŸ’– šŸ’Ŗ šŸ™… šŸš©
pramit_marattha
Pramit Marattha

Posted on December 10, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related