Introduction
Disclaimer
- English is not my native language, so there may be mistakes in the text, but I'm sure that the code can say a thousand times more than any of my words
- I did not use react-map-libraries to make the solution as flexible and understandable as possible
- This is the most simple implementation without deep styling, clustering and global-storage.
- Source code is here - https://github.com/alex1998dmit/map_airbnb
Task
Let's assume that we need to create a map with displaying apartment cards on it as it is done on airbnb
The technology stack is next:
- React & Typescript
- Google map and @googlemaps/react-wrapper
- MUI for style stuff
Implementation
Create our app
Everything is pretty trivial - you need to install the application using creat-react-app using TS
npx create-react-app my-app --template typescript
Install dependecies
For the application to work, we need MUI, @googlemaps/react-wrapper
npm install --save @material-ui/core @material-ui/icons @googlemaps/react-wrapper
Configure map
In this step we will integrate simple map to application. First of all you need to get google-map key - [https://developers.google.com/maps/documentation/javascript/get-api-key].
First of all let's create a Map component, which will be wrapper for google-maps.
import { useEffect, useRef, useState } from "react"; // we will use make styles for styling components, you can use another solutions (like css, sass or cssonjs import { makeStyles } from "@material-ui/core"; // api mock data import Apartments from "./apartments"; // Our component will receive center coords and zoom size in props type MapProps = { center: google.maps.LatLngLiteral zoom: number } // map wrapper styles const useStyles = makeStyles({ map: { height: '100vh' } }) function Map({ center, zoom }: MapProps) { const ref = useRef(null); const [map, setMap] = useState<google.maps.Map<Element> | null>(null) const classes = useStyles(); useEffect(() => { // we need to save google-map object for adding markers and routes in future if (ref.current) { // here will connect map frame to div element in DOM by using ref hook let createdMap = new window.google.maps.Map( ref.current, { center, zoom, disableDefaultUI: true, clickableIcons: false } ); setMap(createdMap) } }, [center, zoom]); // map will be connect to this div block return <div ref={ref} id="map" className={classes.map} />; } export default Map
Then let's modify App.tsx with:
import React, { ReactElement } from 'react'; import { Wrapper, Status } from "@googlemaps/react-wrapper"; import Map from './Map' // Here we can add views when map will loading or failure const render = (status: Status): ReactElement => { if (status === Status.LOADING) return <h3>{status} ..</h3>; if (status === Status.FAILURE) return <h3>{status} ...</h3>; return <></>; }; function App() { if (!process.env.REACT_APP_GOOGLE_KEY) { return <h2>Add google key</h2> } return ( <div className="App"> <Wrapper apiKey={process.env.REACT_APP_GOOGLE_KEY} render={render}> <Map center={{ lat: 55.753559, lng: 37.609218 }} zoom={11} /> </Wrapper> </div> ); } export default App;
And result is:
Add custom overlays
The next step is to add a custom overlay. Why will we use overlays and not markers ? Because in my opinion it will be difficult to customize regular markers, according to the documentation we can change only icon image and label over it [https://developers.google.com/maps/documentation/javascript/custom-markers].
Let's create an OverlayContainer, which will be a wrapper for the components located on the map at certain coordinates.
import * as React from 'react' import ReactDOM from 'react-dom'; // base function for creating DOM div node function createOverlayElement() { const el = document.createElement('div'); el.style.position = 'absolute'; el.style.display = 'inline-block'; el.style.width = '9999px'; return el; } // Our OverlayComponent will recieve map, postion and children props - position is coords, map is google.map object and children is a component that will be render in overlay export type Props = { map: google.maps.Map | null position: { lat: number, lng: number } children?: React.ReactChild } const OverlayContainer = (props: Props) => { const overlay = React.useRef<google.maps.OverlayView | null>(null) const el = React.useRef<Element | null>(null) // modified OverlayView from google.maps [https://developers.google.com/maps/documentation/javascript/reference/3.44/overlay-view?hl=en] class OverlayView extends window.google.maps.OverlayView { position: google.maps.LatLng | null = null; content: any = null; constructor(props: any) { super(); props.position && (this.position = props.position); props.content && (this.content = props.content); } onAdd = () => { if (this.content) this.getPanes().floatPane.appendChild(this.content); }; onRemove = () => { if (this.content?.parentElement) { this.content.parentElement.removeChild(this.content); } }; draw = () => { if (this.position) { const divPosition = this.getProjection().fromLatLngToDivPixel( this.position ); this.content.style.left = divPosition.x + 'px'; this.content.style.top = divPosition.y + 'px'; } }; } React.useEffect(() => { return () => { if (overlay.current) overlay.current.setMap(null) } }, []) if (props.map) { el.current = el.current || createOverlayElement() overlay.current = overlay.current || new OverlayView( { position: new google.maps.LatLng(props.position.lat, props.position.lng), content: el.current } ) overlay.current.setMap(props.map) return ReactDOM.createPortal(props.children, el.current); } return null } export default OverlayContainer
Creating Map Points and Apartment Cards
I will create a simple apartment card by using MUI-core and MUI-icons [https://material-ui.com/ru/components/cards/] [https://material-ui.com/ru/components/material-icons/].
Let's create ApartmentCard:
import React from 'react'; import { makeStyles } from '@material-ui/core/styles'; import Card from '@material-ui/core/Card'; import CardActionArea from '@material-ui/core/CardActionArea'; import CardContent from '@material-ui/core/CardContent'; import CardMedia from '@material-ui/core/CardMedia'; import Typography from '@material-ui/core/Typography'; import AspectRatioIcon from '@material-ui/icons/AspectRatio'; import { Grid, IconButton } from '@material-ui/core'; import MeetingRoomIcon from '@material-ui/icons/MeetingRoom'; import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp'; import CloseIcon from '@material-ui/icons/Close'; const useStyles = makeStyles({ root: { maxWidth: 230, position: 'relative', zIndex: 1001, }, media: { height: 100, }, close: { position: 'absolute', left: 0, top: 0, zIndex: 1001, background: 'white', width: '25px', height: '25px' } }); type ApartmentCardProps = { image: string address: string area: number rooms_number: number floor: number floor_count: number rent: number handleClose: () => void } export default function ApartmentCard(props: ApartmentCardProps) { const classes = useStyles(); return ( <Card className={classes.root}> <IconButton className={classes.close} aria-label="close" onClick={props.handleClose}> <CloseIcon /> </IconButton> <CardActionArea> <CardMedia className={classes.media} image={props.image} title="Contemplative Reptile" /> <CardContent> <Typography variant="body2" component="h2"> {props.address} </Typography> <Grid container spacing={1}> <Grid item container xs={6} spacing={1} alignItems='center'> <Grid item xs={8}><AspectRatioIcon /></Grid> <Grid item xs={4}>{props.area}</Grid> </Grid> <Grid item container xs={6} spacing={1} alignItems='center'> <Grid item xs={8}><MeetingRoomIcon /></Grid> <Grid item xs={4}>{props.rooms_number}</Grid> </Grid> <Grid item container xs={6} spacing={1} alignItems='center'> <Grid item xs={8}><KeyboardArrowUpIcon /></Grid> <Grid item xs={4}>{props.floor}/{props.floor_count}</Grid> </Grid> <Grid item container xs={12} spacing={1} alignItems='center' justifyContent="center"> <Typography variant="body2" style={{ fontWeight: 600 }}>{props.rent} $</Typography> </Grid> </Grid> </Typography> */} </CardContent> </CardActionArea> </Card> ); }
And ApartmentPoint:
import { makeStyles } from "@material-ui/styles" type ApartmentPonitProps = { price: number onClick: () => void } const styles = makeStyles({ root:{ background: 'white', borderRadius: '12px', padding: '8px', width: '60px', zIndex: 1000, position: 'relative' } }) const ApartmentPoint = (props: ApartmentPonitProps) => { const classes = styles() return ( <div className={classes.root} onClick={props.onClick}> {props.price} $ </div> ) } export default ApartmentPoint
We will use MapPoint like wrapper that will render ApartmentPoint or ApartmentCard:
import { useEffect, useRef, useState } from "react" import ApartmentCard from "./ApartmentCard" import ApartmentPoint from "./ApartmentPoint" type MapPointProps = { image: string address: string area: number rooms_number: number floor: number floor_count: number rent: number } const MapPoint = (props: MapPointProps) => { const [opened, setIsOpened] = useState<boolean>(false) const handleOnOpen = () => setIsOpened(true) const handleOnClose = () => setIsOpened(false) const containerRef = useRef<HTMLDivElement>(null) // Hook for handle outside click - simple implementation from stack overflow useEffect(() => { function handleClickOutside(this: Document, event: MouseEvent) { if (containerRef.current && !containerRef.current.contains(event.target as Node)) { setIsOpened(false) } } document.addEventListener("mousedown", handleClickOutside); return () => { document.removeEventListener("mousedown", handleClickOutside); }; }, [containerRef]); return (<div ref={containerRef}> {opened ? <ApartmentCard image={props.image} address={props.address} area={props.area} rooms_number={props.rooms_number} floor={props.floor} floor_count={props.floor_count} rent={props.rent} handleClose={handleOnClose} /> : <ApartmentPoint price={props.rent} onClick={handleOnOpen} />} </div>) } export default MapPoint
And all together
Let's modify Map component by adding apartments points inside overlay containers:
import { makeStyles } from "@material-ui/core"; import { useEffect, useRef, useState } from "react"; import Apartments from "./apartments"; import MapPoint from "./MapPoint"; import OverlayContainer from "./OverlayContainer"; type MapProps = { center: google.maps.LatLngLiteral zoom: number } const useStyles = makeStyles({ map: { height: '100vh' } }) function Map({ center, zoom }: MapProps) { const ref = useRef(null); const [map, setMap] = useState<google.maps.Map<Element> | null>(null) const classes = useStyles(); useEffect(() => { if (ref.current) { let createdMap = new window.google.maps.Map( ref.current, { center, zoom, disableDefaultUI: true, clickableIcons: false } ); setMap(createdMap) } }, [center, zoom]); return <div ref={ref} id="map" className={classes.map}> {Apartments.map((apartment, index) => ( <OverlayContainer map={map} position={{ lat: apartment.lat, lng: apartment.lng }} key={index} > <MapPoint image={apartment.image} address={apartment.address} area={apartment.area} rooms_number={apartment.rooms_number} floor={apartment.floor} floor_count={apartment.floor_count} rent={apartment.rent} /> </OverlayContainer> ))} </div>; } export default Map
Apartments mock-data example(apartments.ts):
const Apartments = [
{
"id": 1,
"image": "https://storage.yandexcloud.net/apartment-images/2.jpg",
"area": 34.9,
"kitchen_area": null,
"address": "Novoalekseevskaya 4d4",
"lat": 55.80562399999999,
"lng": 37.641239,
"rooms_number": 1,
"bedrooms_number": 1,
"restrooms_number": 1,
"floor": 3,
"floor_count": 14,
"rent": 1500
},
{
"id": 2,
"image": "https://storage.yandexcloud.net/apartment-images/10_S939Rcf.jpg",
"area": 47,
"kitchen_area": null,
"address": "Valovaya street 31",
"lat": 55.66497999999999,
"lng": 37.857464,
"rooms_number": 1,
"bedrooms_number": 1,
"restrooms_number": 1,
"floor": 6,
"floor_count": 9,
"rent": 2000
},
{
"id": 3,
"image": "https://storage.yandexcloud.net/apartment-images/07_uvV7gIk.jpg",
"area": 40.9,
"kitchen_area": null,
"address": "academic Volgyn street 8A",
"lat": 55.68271799999999,
"lng": 37.544263,
"rooms_number": 3,
"bedrooms_number": 2,
"restrooms_number": 1,
"floor": 2,
"floor_count": 5,
"rent": 3000
}
]
export default Apartments
Result
P.S.
This is just the first article, in it I tried only to show how I work with google maps and react, in further articles there will be more logic and styling to get as close as possible to airbnb
Top comments (2)
Set Up Your React Project: Start by creating a new React project or using an existing one. You can use tools like Create React App to set up your project quickly.
Install Google Maps API: Sign up for a Google Maps API key and install the necessary packages to integrate Google Maps into your React project. You can use the google-maps-react package, which provides a React wrapper vip restrooms for the Google Maps JavaScript API.
Great! Well done! Thanks for the repo, that really helped me