Utilizando o ModalRoute e fazendo upload de imagens
Esse post é a decima 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
Lá vamos nós novamente, hoje vamos aprender a adicionar novos imóveis no mapa. Lembrando que até aqui já temos a área de SignIn e SignOut, bem como o mapa com a lista de imóveis. Agora sem mais lenga-lenga, boooora para o código.
Começando
Para adicionar um novo imóvel, será preciso adicionar um botão junto ao do logout
e quando ele for clicado exibir um Map Marker e as opções de Adicionar e Cancelar. Neste caso, o Map Marker
ficará fixo, e o usuário movimentará o mapa até achar a posição (latitude e longitude) para adicionar seu imóvel.
A principio será criado um PointReference
para que o Map Marker
seja fixado, então no arquivo, App/styles.js
:
// Importe o polished
import { darken } from "polished";
// ...
export const PointReference = styled.div`
position: absolute;
top: 0;
width: 100vw;
height: 100vh;
pointer-events: none;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
i {
color: #fc6963;
pointer-events: all;
font-size: 50px;
margin-top: 112px;
margin-left: 12px;
-webkit-text-fill-color: #fc6963;
-webkit-text-stroke-width: 2px;
-webkit-text-stroke-color: ${() => darken(0.05, "#fc6963")};
}
div {
margin-top: 100px;
button {
border: none;
font-size: 15px;
height: 46px;
margin: 0 10px;
background-color: #fc6963;
color: #ffffff;
padding: 0 20px;
border-radius: 3px;
pointer-events: all;
text-align: center;
&.cancel {
background: #ffffff;
color: #333333;
}
}
}
`;
Esse PointReference
cria uma layer que ficará sobre o Mapbox, aqui foi utilizado o FontAwesome
para ser o Map Marker
.
Agora é preciso importar o PointReference
e fazer algumas alterações, para exibir ele somente quando o Button
para adicionar imóvel for clicado, e o interessante é que nesse momento haja a listagem de imóveis, deixando assim o mapa mais limpo. No App/index.js
:
// ...
// Adicionar o PointReference
import { Container, ButtonContainer, PointReference } from "./styles";
class Map extends Component {
state = {
viewport: // ...
properties: [],
// Novo state
addActivate: false
};
renderActions() {
return (
<ButtonContainer>
<Button
color="#fc6963"
onClick={() => this.setState({ addActivate: true })}
>
<i className="fa fa-plus" />
</Button>
<Button color="#222" onClick={this.handleLogout}>
<i className="fa fa-times" />
</Button>
</ButtonContainer>
);
}
// ...
}
Ainda falta exibir o render
do PointReference
e também tirar a lista de imóveis do mapa.
class Map extends Component {
// ...
// Novo render
renderButtonAdd() {
return (
this.state.addActivate && (
<PointReference>
<i className="fa fa-map-marker" />
<div>
<button onClick={this.handleAddProperty} type="button">
Adicionar
</button>
<button
onClick={() => this.setState({ addActivate: false })}
className="cancel"
>
Cancelar
</button>
</div>
</PointReference>
)
);
}
render() {
const { containerWidth: width, containerHeight: height, match } = this.props;
const { properties, addActivate } = this.state;
return (
<Fragment>
<MapGL
//...
>
{!addActivate && <Properties match={match} properties={properties} />}
</MapGL>
{this.renderActions()}
{this.renderButtonAdd() /* Esse aqui será adicionado */}
// ...
</Fragment>
);
}
}
Repare que só aparecerá as Properties
quando o addActivate
for false
, e para não poluir o render
foi criada um novo método que adiciona o PointReference
caso o addActivate
seja true
.
E para finalizar essa parte, quando o usuário clicar em adicionar deve ser redirecionado para uma nova página com a latitude
e longitude
presente para adicionar as demais informações sobre o imóvel.
class Map extends Component {
// ...
handleAddProperty = () => {
const { match, history } = this.props;
const { latitude, longitude } = this.state.viewport;
history.push(
`${match.url}/properties/add?latitude=${latitude}&longitude=${longitude}`
);
this.setState({ addActivate: false });
};
// ...
}
A página que o usuário será redirecionado ainda não foi criada (já já será), ela será uma página Modal
igual existe em aplicativos como Instagram e Facebook, onde você não precisa carregar uma nova página quando clica no link, mas com a url
você consegue chegar na mesma.
Adicionar imóvel
Agora o bicho vai PEGAR!!
Agora vamos começar a trabalhar em cima de uma nova página para adicionar um imóvel, essa página será uma página Modal e ela ficará dentro da página App. A ideia de abrir uma página modal é ter uma url exclusiva para ela mas que é “fácil” voltar para a página principal.
Dependências
Para trabalhar com o Modal utilize o react-router-modal
, e além de instalar ele adicione também o seguinte:
npm install react-router-modal react-dropzone stringquery classnames
- react-router-modal: Trabalhar com o modal junto com o
react-router-dom
. - react-dropzone: Permite jogar arquivos selecionados numa determinada área do browser
- stringquery: Ajudará a trabalhar com a
query
de umaurl
. - classnames: Adiciona classes CSS através de objetos.
Agora que você instalou as dependências, é hora de usa-las.
No routes.js
, você deve configurar o seguinte:
import React, { Fragment } from "react";
// ...
import { ModalContainer } from "react-router-modal";
import "react-router-modal/css/react-router-modal.css";
// ...
const Routes = () => (
<BrowserRouter>
<Fragment>
<Switch>
// ...
</Switch>
<ModalContainer />
</Fragment>
</BrowserRouter>
);
// ...
Isso serviu para por o ModalContainer
nas rotas, ele será importante para utilização do Modal
.
Agora é preciso criar uma nova página que adicionará o imóvel, ela conterá apenas um formulário e através da url
receberá a latitude
e a longitude
. Então monte aquela estrutura marota de páginas:
src/pages/
|--- AddProperty/
|--- index.js
|--- styles.js
Depois da estrutura montada, já comece estilizando com o seguinte código no styles.js
:
import styled from "styled-components";
import Dropzone from "react-dropzone";
export const File = styled(Dropzone)`
border: 2px dashed #ff3333;
width: 100%;
max-width: 660px;
font-size: 16px;
color: #777777;
text-align: center;
display: grid;
grid-template-columns: 100px 100px 100px;
grid-gap: 5px;
background-color: #fff;
color: #444;
&.without-files {
display: flex;
}
img {
width: 100px;
}
p {
margin-top: 15px;
border: none !important;
}
`;
Aqui você percebe o StyledComponents
(s2) trabalhando de forma mágica, mais uma vez. Dessa vez não foi utilizado um elemento do styled
e sim um outro componente (Dropzone
), isso foi possível porque o Dropzone é um elemento React, mas foi só chama-lo como parâmetro do styled
que foi sucesso!!!
Ainda no styles.js
será necessário estilizar o form
, mas devido ao código está muito extenso copie desse link aqui. Só não vai me esquecer de copiar rsrs.
Agora é hora da página, então no index.js
comece importando alguns componentes:
import React, { Component } from "react";
import { withRouter } from "react-router-dom";
import querySearch from "stringquery";
import classNames from "classnames";
import PropTypes from "prop-types";
import { Form, File } from "./styles";
import api from "../../services/api";
Na class além dos states
será validado no componenteDidMount
se na query
da url
os parâmetros latitude
e longitude
estão presentes, caso não esteja, ele enviar o usuário para a página do App. Então ainda no index.js
:
class AddProperty extends Component {
static propTypes = {
location: PropTypes.shape({
search: PropTypes.string
}).isRequired,
history: PropTypes.shape({
push: PropTypes.func
}).isRequired
};
state = {
title: "",
address: "",
price: "",
error: "",
files: []
};
componentDidMount() {
const params = querySearch(this.props.location.search);
if (
!params.hasOwnProperty("latitude") ||
!params.hasOwnProperty("longitude")
) {
alert("É necessário definir a latitude e longitude para um imóvel.");
this.props.history.push("/app");
}
this.setState({ ...params });
}
// ainda tem mais
Lá no styles
o componente Dropzone
foi importado, lembra? Ele é o componente em que separa uma área na página e dentro dela o usuário pode adicionar arquivos arrastando de pastas externas ao Browser e jogando dentro dela ou mesmo clicar para abrir o gerenciador de arquivos.
Os próximos métodos trabalham com o Dropzone
, um para colocar os arquivos no state
e o outro para renderizar esses arquivos.
handleDrop = files => this.setState({ files });
renderFiles() {
const { files } = this.state;
return !files.length ? (
<p>Jogue as imagens ou clique aqui para adiciona-las</p>
) : (
files.map(file => <img key={file.name} src={file.preview} />)
);
}
Agora será trabalhado com o evento do handlerSubmit
que é disparado no onSubmit
, para ficar mais claro foi dividido em duas partes:
handleSubmit = async e => {
e.preventDefault();
try {
const { title, address, price, latitude, longitude, files } = this.state;
if (!title || !address || !price || !latitude || !longitude) {
this.setState({ error: "Preencha todos os campos" });
return;
}
const { data: { id } } = await api.post("/properties", {
title,
address,
price,
latitude,
longitude
});
// tem mais
As considerações sobre a primeira parte são as seguintes, será utilizado métodos assíncronos e por isso o uso do async/await
e já foi removido o envio padrão do formulário para uma action
com o e.preventDefault()
.
Dentro try/catch
é trabalhado com as requisições e para elas foram extraídas do state
as variáveis importantes e tão logo verifica se estão presentes para dar continuidade a função.
É importante notar que o já foi extraído o id
, através da desestruturação, da resposta da requisição da API para criar o imóvel.
A segunda parte desse método, não tem segredo, bora disseca-la:
// Continuação do handleSubmit
if (!files.length) this.props.history.push("/app");
const data = new FormData();
files.map((file, index) =>
data.append(`image[${index}]`, file, file.name)
);
const config = {
headers: {
"content-type": "multipart/form-data"
}
};
await api.post(
`/properties/${id}/images`,
data,
config
);
this.props.history.push("/app");
} catch (err) {
this.setState({ error: "Ocorreu algum erro ao adicionar o imóvel" });
}
};
Ainda dentro do try/catch
nessa segunda parte temos o seguinte, uma verificação se existe algum arquivo (files)
adicionado ao state
, não existindo o usuário será enviado para a página App.
Todavia, existindo arquivos, foi utilizado o FormData
para estrutura-los de maneira que um formulário deve está. Para essa requisição, por estar trabalhando com um formulário de arquivos, o cabeçalho deve conter o content-type
como multipart/form-data
. Feita a requisição à API, para adicionar os arquivos ao id
definido anteriormente o usuário é enviado para a página de App.
Por fim, caso exista algum problema nas requisições um erro no formulário é apresentado ao usuário.
Ainda dentro do index.js
, o método handleCancel
deve ser adicionado:
// ...
handleCancel = e => {
e.preventDefault();
this.props.history.push("/app");
};
Esse método apenas mandará o usuário para a pagina App.
E para finalizar a página AddProperty
, você deve adicionar o render
:
// ...
render() {
return (
<Form onSubmit={this.handleSubmit}>
<h1>Adicionar imóvel</h1>
<hr />
{this.state.error && <p>{this.state.error}</p>}
<input
type="text"
placeholder="Título"
onChange={e => this.setState({ title: e.target.value })}
/>
<input
type="text"
placeholder="Endereço"
onChange={e => this.setState({ address: e.target.value })}
/>
<input
type="decimal"
placeholder="Preço"
onChange={e => this.setState({ price: e.target.value })}
/>
<File
multiple
onDrop={this.handleDrop}
className={classNames({ "without-files": !this.state.files.length })}
>
{this.renderFiles()}
</File>
<div className="actions">
<button type="submit">Adicionar</button>
<button onClick={this.handleCancel} className="cancel">
Cancelar
</button>
</div>
</Form>
);
}
}
export default withRouter(AddProperty);
Aqui é simplesmente um formulário utilizando os componentes previamente estilizados. Talvez você se pergunte sobre esse trecho de código: classNames({ "without-files": !this.state.files.length })
, nele a função classNames
serve para trabalhar com classes do CSS a partir de um objeto. O classNames
funciona assim, caso o valor da chame seja true
ele entende que aquela classe deve ser adicionado ao className
.
Chamando a página
Agora você está a um passo de concluir esse Post. Falta ainda chamar essa página que acabamos de criar em algum lugar.
Ela será chamada dentro da página App, já que é um Modal da página é lá que o AddProperty
deve ficar. Então no App/index.js
comece importando algumas coisas:
// ...
import { ModalRoute } from "react-router-modal";
// ...
import AddProperty from "../AddProperty";
O ModalRoute
fará o papel do Route
definindo o path
e o component
e como o nome sugere será para o Modal.
Agora dentro do render
do componente App, adicione:
// ...
{this.renderActions()}
{this.renderButtonAdd()}
<ModalRoute
path={`${match.url}/properties/add`}
parentPath={match.url}
component={AddProperty}
/>
// ...
No ModalRoute
o path
é a url
que executará o component
que é a página AddProperty, já o parentPath
identifica qual é a página que chamou o Model e quando ele for fechado, por exemplo, retorna para a página do parentPath
.
Finished it
É meu caro essa é a pior parte da brincadeira, mas fica tranquilo que ainda tem mais!!!
Hoje você aprendeu a utilizar RouteModal e já consegue adicionar o imóvel pelo APP. Irado, não?
Espero que você tenha gostado desse Post, caso tenha alguma dúvida deixe aí nos comentários!! E pode compartilhar ele como se não tivesse amanhã!!!!
O código está aqui.
Aquele abraço bem apertado e até mais.