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.
- Parte 1: Iniciando com AdonisJS: Autenticação JWT e API REST;
- Parte 2: Criando CRUD e relações em API REST no AdonisJS;
- Parte 3: Upload de imagens e geolocalização no AdonisJS;
- Parte 4: Iniciando com React Native: Navegação e Autenticação com JWT;
- Parte 5: Instalando o Mapbox e listando imóveis no React Native;
- Parte 6: Instalando a Câmera e realizando o cadastro de Imóveis;
- Parte 7: Listando em um Modal os dados detalhados dos Imóveis;
- Parte 8: Iniciando com ReactJS: Navegação e Autenticação com JWT;
- Parte 9: Instalando o Mapbox e listando os imóveis no ReactJS
- Parte 10: Utilizando o ModalRoute e fazendo upload de imagens
- Parte 11: Exibindo informações do imóvel com ModalRoute
- em breve…
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 onViewportChange
conseguimos 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 width
e 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!!