Instalando o Mapbox e listando os imóveis no ReactJS

Esse post é a nona parte da série de posts “Clone AirBnB com AdonisJS, React Native e ReactJS” onde iremos construir do zero uma aplicação web com ReactJS e também uma aplicação mobile com React Native com dados servidos através de uma API REST feita com NodeJS utilizando o framework AdonisJS.

Aqui estamos em mais um post dessa série sensacional, nesse ponto já conseguimos criar e logar com o nosso usuário. Agora nos falta um mapa e a lista dos imóveis e é exatamente isso que faremos.

Primeiro passo

Será utilizado neste post o mapa já conhecido pela comunidade, o Mapbox. Primeiro é necessário criar uma conta no site da Mapbox. Uma vez criada a conta, basta copiar o access token, na área “Get your access token”:

App

Essa será a página central do aplicativo, e é em cima dela que será construído o App. Para facilitar o trabalho com a utilização do Mapbox no ReactJS usaremos uma lib mantida pelo Uber, o react-map-gl e o react-dimensions que depois explico sobre ele.

npm install react-map-gl react-dimensions

Agora você precisa configurar a página seguindo o mesmo padrão até aqui:

src/pages/
      |--- App/
              |--- index.js
              |--- styles.js

Comece com o styles.js:

import styled from "styled-components";

export const Container = styled.div`
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100vh;
`;

Neste componente é preciso usar o PropTypes para validar os valores de entrada do componente, isso é uma boa prática e te ajudará caso esqueçade passar alguma propriedade ou mesmo um valor errado. Os PropTypes ficarão no static propTypes. Clique aqui para saber mais sobre ele. No index.js:

import React, { Component } from "react";
import Dimensions from "react-dimensions";
import { Container } from "./styles";
import MapGL from "react-map-gl";
import PropTypes from "prop-types";

const TOKEN =
  "SEU access token AQUI";

class Map extends Component {
  static propTypes = {
    containerWidth: PropTypes.number.isRequired,
    containerHeight: PropTypes.number.isRequired
  };

  state = {
    viewport: {
      latitude: -27.2108001,
      longitude: -49.6446024,
      zoom: 12.8,
      bearing: 0,
      pitch: 0
    }
  };
  render() {
    const { containerWidth: width, containerHeight: height } = this.props;
    return (
      <MapGL
        width={width}
        height={height}
        {...this.state.viewport}
        mapStyle="mapbox://styles/mapbox/dark-v9"
        mapboxApiAccessToken={TOKEN}
        onViewportChange={viewport => this.setState({ viewport })}
      />
    );
  }
}

const DimensionedMap = Dimensions()(Map);
const App = () => (
  <Container>
    <DimensionedMap />
  </Container>
);

export default App;

Observe que as informações como longitude e latitude foram mantidas no estado do Componente, pois com o evento onViewportChangeconseguimos mudar a posição do mapa na página. Outra coisa é que usamos o Dimensions para conseguir aumentar o tamanho do mapa para o tamanho inteiro da página, isso acontece pois é preciso definir um widthe um height, exigência do react-map-gl, e sem ele não saberíamos o tamanho exato.

Só para reforçar, o Dimensions pega a dimensão do componente pai, então temos o Container que é o elemento pai do Dimensions e que ocupa toda a tela, então esse width e height é passado para o react-map-gl.

Adicione no routes.js:

import App from "./pages/App";

// ...
// De
<PrivateRoute path="/app" component={() => <h1>App</h1>} />
// Para
<PrivateRoute path="/app" component={App} />

Funcionalidades

Agora é preciso listar os móveis sobre o mapa, para quando clicar saber as informações sobre esse eles. Para isso você deve buscar na API a lista de propriedades e criar o componente que vai conter as informações da delas.

Componente Properties

Primeiro você deve criar um diretório que conterá os componentes relacionados ao App, e já adicionar o componente Properties:

App/
 |--- components/
     |--- Properties/
            |--- index.js
            |--- styles.js

No index.js, terá apenas uma lista com o valor dos imóveis. Para trabalhar com o valor do Real será utilizado o objeto Intl.NumberFormat, que serve para transformar números em moedas. É bom reparar o Marker também, já que ele será importante para fixar o preço no mapa quando movimentarmos e permanecer na posição dele no mapa. Por fim, será utilizado o (lindo) StyledComponent para estilizar a div com o nome de Pin. Não se preocupe com o Link, ele servirá para nós mais tarde.

No index.js:

import React from "react";
import { Marker } from "react-map-gl";
import { Link } from "react-router-dom";
import PropTypes from "prop-types";

import { Pin } from "./styles";

const intlMonetary = new Intl.NumberFormat("pt-BR", {
  style: "currency",
  currency: "BRL",
  minimumFractionDigits: 2
});

const Properties = ({ properties }) =>
  properties.map(property => (
    <Marker
      key={property.id}
      longitude={property.longitude}
      latitude={property.latitude}
    >
      <Pin>
        <Link to="">{intlMonetary.format(property.price)}</Link>
      </Pin>
    </Marker>
  ));

Properties.propTypes = {
  properties: PropTypes.arrayOf(
    PropTypes.shape({
      id: PropTypes.number,
      title: PropTypes.string,
      price: PropTypes.number,
      longitude: PropTypes.number,
      latitude: PropTypes.number
    })
  ).isRequired
};

export default Properties;

Nesse componente foi utilizado dois PropTypes bem comuns. O PropTypes.arrayOf define que aquela propriedade será um Array e o PropTypes.shape que é utilizado para definir pelo menos as keys mais importantes de um Object que devem estar na propriedade do componente.

No styles.js:

import styled from "styled-components";
export const Pin = styled.div`
  padding: 5px;
  background: #fc6963;
  border: 0;
  border-radius: 5px;
  a {
    color: #fff;
    font-size: 14px;
    text-decoration: none;
  }
`;

Listando os móveis

Agora no App/index.js você precisa consumir os dados da API do AdonisJS, que já foi criada, colocar as properties no estado do componente e exibi-la através do componente que acabou de ser criado. Bora fazer isso então?

No index.js importaremos o seguinte:

// ...
import debounce from "lodash/debounce";
import api from "../../services/api";

import Properties from "./components/Properties";

Sobre o debounce, talvez você esteja se perguntando: Mas que raios é isso? Calma, ele é uma função que retarda a execução de uma outra função passada em um tempo determinado. No nosso caso, toda vez que o mapa for movimentado queremos pegar de acordo com a longitude e latitude as novas propriedades, só tem um porém. Imagina fazer uma requisição toda vez que o estado do mapa for alterado? O que ocorre dezenas e as vezes centena de vezes por segundo. O servidor ou Browser acabariam entrando em parafuso. É nesse ponto que o debounce entra em cena, toda a vez que a função updatePropertiesLocalization (já já você verá ela) o debounce pega a última chamada dela, se em um 500 milissegundos não existir nenhuma outra chamada, ela busca na API as properties de acordo com a nova localização!!! Massa, né?

Dentro do componente App, adicione um construtor:

constructor() {
  super();
  this.updatePropertiesLocalization = debounce(
    this.updatePropertiesLocalization,
    500
  );
}

Ele será útil para reescrever a função do updatePropertiesLocalization, que agora contará com o debounce encapsulando ela.

Adicione também no state:

state = {
    viewport: {
      // ...
    },
    // Adicione esse state
    properties: []
  };

Ainda dentro do componente App:

componentDidMount() {
  this.loadProperties();
}

updatePropertiesLocalization() {
  this.loadProperties();
}

loadProperties = async () => {
  const { latitude, longitude } = this.state.viewport;
  try {
    const response = await api.get("/properties", {
      params: { latitude, longitude }
    });
    this.setState({ properties: response.data });
  } catch (err) {
    console.log(err);
  }
};

No componenteDidMount ,que é chamado assim que o componente é montado, executa a loadProperties para resgatar as properties da API. No updatePropertiesLocalization também executa a função responsável por pegar os dados na API. Na função loadProperties a latitude e longitude que está no estado do componente são resgatadas e utilizadas como parâmetros da requisição, observe que não foi preciso passar o token, visto que isso já foi configurado lá na criação da api com o interceptor.

E para finalizar, ajuste o render:

render() {
  const { containerWidth: width, containerHeight: height } = this.props;
  const { properties } = this.state;
  return (
    <MapGL
      width={width}
      height={height}
      {...this.state.viewport}
      mapStyle="mapbox://styles/mapbox/dark-v9"
      mapboxApiAccessToken={TOKEN}
      onViewportChange={viewport => this.setState({ viewport })}
      onViewStateChange={this.updatePropertiesLocalization.bind(this)}
    >
      <Properties properties={properties} />
    </MapGL>
  );
}

No render as properties são pegas do estado do componente para preencher a lista do componente Properties. Foi utilizado o onViewStateChange para chamar a função responsável por resgatar as novas properties assim que o mapa altera a sua posição, lembre-se que respeitando o debounce.

Dando tchau

Calma aí, que é o logout!!!

Pra finalizar esse Post, é preciso adicionar um botão para dar LogOut. Para isso, deve ser criado um novo componente, no mesmo diretório que ficou o Properties :

App/
 |--- components/
     |--- Button/
            |--- index.js
            |--- styles.js

No index.js :

import React from "react";
import PropTypes from "prop-types";

import { Button } from "./styles";

const CustomButton = ({ children, color, ...props }) => (
  <Button type="button" color={color} {...props}>
    {children}
  </Button>
);

CustomButton.propTypes = {
  children: PropTypes.element.isRequired,
  color: PropTypes.string.isRequired,
  props: PropTypes.object
};

export default CustomButton;

Sobre o componente não tenho muito a dizer, mas sobre a estilização… Primeiro observe a lib polished, com ela temos diversas funcionalidadespara serem utilizadas no CSS, aqui foi utilizada a darken que trabalha em cima de uma cor e de acordo com um range ele retorna a cor passada escurecida.

// Não se esqueça 
npm install polished

Outra coisa sensacional, é possível ter acesso as propriedades do componente dentro da estilização, utilizando ${({ color }) => color} , que nesse caso o color foi desestruturado, dessa maneira fica fácil pegar a cor recebida no Button e usar o darken quando fizer o hover e active.

import styled from "styled-components";
import { darken } from "polished";

export const Button = styled.button`
  width: 60px;
  height: 60px;
  border-radius: 60px;
  border: none;
  background-color: ${({ color }) => color};
  margin-top: 10px;
  color: #fff;
  i {
    font-size: 18px;
  }
  &:hover {
    background-color: ${({ color }) => darken(0.05, color)};
  }
  &:active {
    background-color: ${({ color }) => darken(0.07, color)}
  }
`;

Aproveitando o embalo da estilização, vá em no App/styles.js e adicione o código:

// ...

export const ButtonContainer = styled.div`
  position: absolute;
  bottom: 20px;
  right: 10px;
  display: flex;
  flex-direction: column;
`;

No ButtonContainer ficará os Button principais.

Agora no index.js da Page App, adicionar o seguinte:

import React, { Component, Fragment } from "react";
// ...
import { withRouter } from "react-router-dom";

import { logout } from "../../services/auth";

// ...

import Button from "./components/Button";
import { Container, ButtonContainer } from "./styles";

Foi importado o Fragment que serve apenas para encapsular outros elementos, além disso o withRouter também foi importado para cuidar da navegação, o logout que será responsável por remover o token, o componente Button customizado e o ButtonContainer para deixar o Button no canto inferior da tela.

Ainda no index.js:

// ...
class Map extends Component {
  // ...
  handleLogout = e => {
    logout();
    this.props.history.push("/");
  };

  renderActions() {
    return (
      <ButtonContainer>
        <Button color="#222" onClick={this.handleLogout}>
          <i className="fa fa-times" />
        </Button>
      </ButtonContainer>
    );
  }
  // ...
  
  render() {
    // ...
    return (
      <Fragment>
        <MapGL>
          // ...
        </MapGL>
        {this.renderActions()}
      </Fragment>
    )
  }
}

// De
const DimensionedMap = Dimensions()(Map);
// Para
const DimensionedMap = withRouter(Dimensions()(Map));

Para separar um pouco as coisas foi criada a função renderActions que conterá os Button que eventualmente estiverem na parte inferior. O render foi ajustado para receber os elementos “lado a lado” com o Fragment.

Foi criada a função handlerLogout que chama o logout e depois manda o usuário para a página inicial.

Por fim, agora o DimensionedMap possui também o withRouter, que como eu havia falado, serve para adicionar o history que trata da navegação para nós.

Por hoje é só!!!

Cara o App está ficando bruto, não??

Nesse post, vimos como adicionar o mapa com Mapbox, além de listar os imóveis nele. Também deslogamos o usuário da sessão que ele estava.

Código desse post está aqui.

Se você está gostando dessa série mais querida que a série do Brasileirão deixe o seu comentário e não se esqueça de compartilhar com aquele seu amigo dev.

Ah, e ainda tem mais então fica de olho no blog!!!

Abraços!!