2024-09-09 Web Development
Google Map Viewer in React
By O Wolfson
In this article, we'll explore how to create a Google Map Viewer using React and Next.js. This app allows users to input a Google Maps link, which then displays the corresponding location on a map. You can check out the demo here.
Get the source code on GitHub.
Project Structure
Our project consists of a main page (app/page.tsx
) and four components located in the components/maps
folder.
plaintextapp/ page.tsx components/ maps/ google-map.tsx map-component.tsx map-provider.tsx map-utils.ts
Let's walk through these components and their functionalities in a logical order.
1. map-utils.ts
This utility module handles URL expansion and extraction of latitude and longitude coordinates from Google Maps links. It contains two main functions:
expandUrl
: Expands shortened URLs to their full version.extractLatLong
: Extracts latitude and longitude coordinates from either DMS (Degrees, Minutes, Seconds) or decimal formats.
tsx"use server";
import axios from "axios";
export const expandUrl = async (shortUrl: string): Promise<string | null> => {
try {
const response = await axios.get(shortUrl, {
maxRedirects: 0,
validateStatus: (status) => status === 302, // Handle redirect
});
return response.headers.location; // This contains the expanded URL
} catch (error) {
console.error("Error expanding URL:", error);
return null;
}
};
const dmsToDecimal = (
degrees: number,
minutes: number,
seconds: number,
direction: string
): number => {
const decimal = degrees + minutes / 60 + seconds / 3600;
return direction === "S" || direction === "W" ? -decimal : decimal;
};
export const extractLatLong = (
url: string
): { lat: number; lng: number } | null => {
// Decode the URL to handle URL-encoded characters
const decodedUrl = decodeURIComponent(url);
// console.log("Decoded URL:", decodedUrl);
// Regex to match DMS coordinates
const dmsPattern = /(\d{1,3})°(\d{1,2})'(\d{1,2}(?:\.\d+)?)"?([NSEW])/g;
// Regex to match decimal coordinates
const decimalPattern = /@(-?\d+\.\d+),(-?\d+\.\d+)/;
// Regex to match accurate 3d/4d coordinates
const accurateDecimalPattern = /3d(-?\d+\.\d+)!4d(-?\d+\.\d+)/;
let match;
let lat, lng;
// Try to extract accurate 3d/4d coordinates
match = accurateDecimalPattern.exec(decodedUrl);
if (match) {
return {
lat: parseFloat(match[1]),
lng: parseFloat(match[2]),
};
}
// Try to extract DMS coordinates for latitude and longitude
const dmsMatches = decodedUrl.match(dmsPattern);
if (dmsMatches && dmsMatches.length >= 2) {
const latMatch = dmsMatches[0].match(
/(\d{1,3})°(\d{1,2})'(\d{1,2}(?:\.\d+)?)"?([NSEW])/
);
const lngMatch = dmsMatches[1].match(
/(\d{1,3})°(\d{1,2})'(\d{1,2}(?:\.\d+)?)"?([NSEW])/
);
if (latMatch && lngMatch) {
lat = dmsToDecimal(
parseInt(latMatch[1]),
parseInt(latMatch[2]),
parseFloat(latMatch[3]),
latMatch[4]
);
lng = dmsToDecimal(
parseInt(lngMatch[1]),
parseInt(lngMatch[2]),
parseFloat(lngMatch[3]),
lngMatch[4]
);
return { lat, lng };
}
}
// Fallback to extract decimal coordinates if accurate coordinates are not found
match = decimalPattern.exec(decodedUrl);
if (match) {
return {
lat: parseFloat(match[1]),
lng: parseFloat(match[2]),
};
}
return null;
};
2. map-provider.tsx
The MapProvider
component loads the Google Maps JavaScript API and provides it to the rest of the app. This component uses the useJsApiLoader
hook from the @react-google-maps/api
library to load the API asynchronously.
tsx"use client";
import { Libraries, useJsApiLoader } from "@react-google-maps/api";
import { ReactNode } from "react";
const libraries = ["places", "drawing", "geometry"];
export function MapProvider({ children }: { children: ReactNode }) {
const { isLoaded: scriptLoaded, loadError } = useJsApiLoader({
googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API as string,
libraries: libraries as Libraries,
});
if (loadError) return <p>Encountered error while loading google maps</p>;
if (!scriptLoaded) return <p>Map Script is loading ...</p>;
return children;
}
3. map-component.tsx
The MapComponent
is the core component that renders the map, marker, and info window. It also provides an input form for users to enter a Google Maps link, title, and address.
tsx"use client";
import React, { useState, useEffect } from "react";
import { GoogleMap, Marker, InfoWindow } from "@react-google-maps/api";
import Link from "next/link";
import { expandUrl, extractLatLong } from "./map-utils";
const defaultMapOptions = {
zoomControl: true,
tilt: 0,
gestureHandling: "auto",
mapTypeId: "roadmap",
};
const defaultMapZoom = 18;
const info = {
title: "The Empire State Building",
address: "20 W 34th St., New York, NY 10001, United States",
link: "https://maps.app.goo.gl/qz2zoCrJpmjH7Pmk7",
};
export interface MapCenter {
lat: number;
lng: number;
}
export const defaultMapContainerStyle = {
height: "300px",
borderRadius: "15px 15px 15px 15px",
};
const MapComponent: React.FC = () => {
const [isOpen, setIsOpen] = useState(false);
const [mapCenter, setMapCenter] = useState<MapCenter>();
const [mapInfo, setMapInfo] = useState(info);
const [tempMapInfo, setTempMapInfo] = useState(info);
const [isLoading, setIsLoading] = useState(false);
const [isSuccess, setIsSuccess] = useState(false);
useEffect(() => {
const fetchCoordinates = async () => {
const expandedUrl = await expandUrl(mapInfo.link);
if (expandedUrl) {
const coords = await extractLatLong(expandedUrl);
if (coords) {
setMapCenter(coords);
}
}
};
if (mapInfo.link) {
fetchCoordinates();
}
}, [mapInfo.link]);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);
setIsSuccess(false);
setMapInfo(tempMapInfo);
setIsLoading(false);
setIsSuccess(true);
setTimeout(() => setIsSuccess(false), 1500);
};
return (
<div className="flex flex-col gap-4">
<GoogleMap
mapContainerStyle={defaultMapContainerStyle}
center={mapCenter}
zoom={defaultMapZoom}
options={defaultMapOptions}
>
{mapCenter && (
<>
<Marker position={mapCenter} onClick={() => setIsOpen(true)} />
{isOpen && (
<InfoWindow
position={mapCenter}
onCloseClick={() => setIsOpen(false)}
>
<div>
<div className="text-black flex flex-col gap-1">
<div className="font-semibold text-lg -mb-1">
{mapInfo.title}
</div>
<div>{mapInfo.address}</div>
<Link
target="_blank"
href={mapInfo.link}
className="font-semibold text-blue-500 hover:underline"
>
View on Google Maps
</Link>
</div>
</div>
</InfoWindow>
)}
</>
)}
</GoogleMap>
<form onSubmit={handleSubmit} className="flex flex-col gap-2">
<div>
<label htmlFor="mapLink" className="block text-sm p-1">
Map Link
</label>
<input
id="mapLink"
type="text"
value={tempMapInfo.link}
onChange={(e) =>
setTempMapInfo({ ...tempMapInfo, link: e.target.value })
}
placeholder="Enter the map link"
className="border border-gray-300 rounded-md p-2 w-full text-black"
/>
</div>
<div>
<label htmlFor="mapTitle" className="block text-sm p-1">
Map Title
</label>
<input
id="mapTitle"
type="text"
value={tempMapInfo.title}
onChange={(e) =>
setTempMapInfo({ ...tempMapInfo, title: e.target.value })
}
placeholder="Enter the map title"
className="border border-gray-300 rounded-md p-2 w-full text-black"
/>
</div>
<div>
<label htmlFor="mapAddress" className="block text-sm p-1">
Map Address
</label>
<input
id="mapAddress"
type="text"
value={tempMapInfo.address}
onChange={(e) =>
setTempMapInfo({ ...tempMapInfo, address: e.target.value })
}
placeholder="Enter the map address"
className="border border-gray-300 rounded-md p-2 w-full text-black"
/>
</div>
<button
type="submit"
className="mt-4 bg-blue-500 text-white p-2 rounded-md active:bg-blue-400"
disabled={isLoading}
>
{isLoading ? "Submitting..." : "Submit"}
</button>
{isSuccess && (
<div className="text-green-500 mt-2">
Map information updated successfully!
</div>
)}
</form>
</div>
);
};
export { MapComponent };
4. google-map.tsx
The GoogleMap
component serves as a container for the MapProvider
and MapComponent
. It ensures that the Google Maps API is loaded before rendering the map.
tsximport React from "react";
import { MapProvider } from "@/components/maps/map-provider";
import { MapComponent } from "@/components/maps/map-component";
function GoogleMap() {
return (
<div>
<MapProvider>
<MapComponent />
</MapProvider>
</div>
);
}
export default GoogleMap;
Main Page (app/page.tsx
)
Finally, the main page of our Next.js app (app/page.tsx
) imports and uses the GoogleMap
component.
tsximport GoogleMap from "@/components/maps/google-map";
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<div className="w-full">
<GoogleMap />
</div>
</main>
);
}
Conclusion
By structuring our components logically and leveraging the power of React and Next.js, we have created a robust Google Map Viewer application. The MapComponent
handles user input and displays the map, while MapProvider
ensures the Google Maps API is loaded correctly. The map-utils.ts
module provides essential utility functions for URL and coordinate handling.
Feel free to explore the demo and try out different Google Maps links. Happy coding!