Conteúdos desta aula:
- Criando o projeto 00:01:58
- Incluindo o Google Maps 00:07:34
- Setando nossa localização atual com o Pin 00:10:27
- Deixando nossa API disponível na internet 00:25:04
- Adicionando Marcadores de cafeterias mais próximas 00:29:04
- Criando o Component de estabelecimento selecionado 00:43:06
- Puxando as avaliações da nossa API 01:05:24
- Criando o Dropdown de cafés mais próximos 01:17:36
- Listando os cafés próximos mais amados 01:25:54
Acompanhe pelo código fonte para resolver Bugs simples: https://github.com/OneBitCodeBlog/find-my-coffee-mobile-prod
Referências para se aprofundar no React Native
1 – Documentação do Expo: https://docs.expo.io
2 – Flexbox: https://reactnative.dev/docs/flexbox
Criando o projeto
1 – Entre na pasta find-my-coffee, e digite o comando abaixo:
1 |
npx create-react-native-app find-my-coffee-mobile |
2 – Gere o “Default new app”.
Setando o google maps
1 – Instale o google maps com o seguinte comando:
1 |
yarn add react-native-maps |
2 – Agora vamos criar as pastas /src/components/GoogleMaps.
3 – Dentro do componente APP cole o seguinte código:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
import React from 'react'; import { View, StyleSheet } from 'react-native'; const App = () => { return ( <View style={styles.container}> </View> ); }; const styles = StyleSheet.create({ container: { flex: 1, zIndex: 0, }, }); export default App; |
7 – Agora inclua no component APP:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import React from 'react'; import { View, StyleSheet, Alert } from 'react-native'; import MapView, { Marker } from 'react-native-maps'; const App = () => { return ( <View style={styles.container}> <MapView style={styles.map}> </MapView> </View> ); }; const styles = StyleSheet.create({ container: { flex: 1, zIndex: 0, }, map: { height: '100%', width: '100%', }, }); export default App; |
8 – Repare que nosso mapa foi carregado \o/
Setando nossa localização atual com o Pin
1 – No componente App, agora vamos setar nossa localização atual com as seguintes inclusões:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
import React, { useState, useEffect } from 'react'; import * as Location from 'expo-location'; import MapView, {Marker} from 'react-native-maps'; ... const App = () => { const [latitude, setLatitude] = useState(0); const [longitude, setLongitude] = useState(0); useEffect(() => { let {status} = await Location.requestPermissionsAsync(); if(status !== 'granted') { Alert.alert('Ative as permissões de uso do GPS para acessar o APP') } else { let location = await Location.getCurrentPositionAsync({}); setLatitude(location.coords.latitude); setLongitude(location.coords.longitude); } })(); }, []); ... return ( <View style={styles.container}> <MapView style={styles.map} region={ { latitude: (latitude != 0) ? latitude : 0, longitude: (longitude != 0) ? longitude : 0, latitudeDelta: 0.03, longitudeDelta: 0.03, } }> </MapView> </View> ); }; ... |
2 – Perceba que adicionamos a função setCurrentLocation() e setamos as variáveis ‘latitude’ e ‘longitude’, usando essa função no useEffect e adicionando o atributo “region” ao nosso MapView. Agora seu mapa será carregado na sua localização atual. \o/
3 – Baixe as seguintes imagens (link1 link2 link3), cria a pasta /src/images e coloque nela
4 – Atualize o APP.js colocando:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
... <MapView zoom={15} style={styles.map} region={ { latitude: (latitude != 0) ? latitude : 0, longitude: (longitude != 0) ? longitude : 0, latitudeDelta: 0.03, longitudeDelta: 0.03, } }> <Marker title="Seu local" icon={require('./images/my-location-pin.png')} coordinate={ { latitude: (latitude != 0) ? latitude : 0, longitude: (longitude != 0) ? longitude : 0 } } /> </MapView> ... |
Deixando nossa API disponível na internet
Para podermos testar via mobile o nosso APP, vamos precisar deixar nossa API disponível na internet, a maneira mais fácil de fazer isso em um ambiente de testes é usando o ngrok, com um comando você pluga sua api local com um endpoint.
1 – Baixe o ngrok aqui: https://ngrok.com/download
2 – Execute o seguinte comando em seu terminal (na pasta do ngrok):
1 |
ngrok http 3001 |
Ele retornará algo como:
1 2 3 4 5 6 7 8 9 10 11 12 |
ngrok by @inconshreveable (Ctrl+C to quit) Session Status online Session Expires 6 hours, 42 minutes Version 2.3.35 Region United States (us) Web Interface http://127.0.0.1:4040 Forwarding http://66ecce188f1f.ngrok.io -> http://localhost:3001 Forwarding https://66ecce188f1f.ngrok.io -> http://localhost:3001 Connections ttl opn rt1 rt5 p50 p90 4 0 0.00 0.00 23.57 28.58 |
3 – Copie o endereço https, pois usaremos ele em nosso service.
4 – Na API, em config/environments/development.rb coloque:
1 2 3 |
Rails.application.configure do config.hosts.clear end |
5 – Vamos fazer um pequeno ajuste no Service get_google_coffee_list atualizando a URL que ele chama para melhorar nossa busca:
1 |
base_url = "https://maps.googleapis.com/maps/api/place/textsearch/json?types=cafe&#{location}&#{radius}&#{key}" |
6 – Suba novamente o servidor:
1 |
rails s -p 3001 |
Adicionando Marcadores de cafeterias mais próximas
1 – Realize a instalação do axios com o comando abaixo:
1 |
npm install axios |
2 – Crie a pasta /src/services.
3 – Dentro da pasta /src/services, crie os arquivos ‘establishments_service.js’ e ‘api.js’.
4 – Cole o seguinte código no arquivo ‘api.js’:
1 2 3 4 5 |
import axios from 'axios'; const Api = axios.create({baseURL: 'SEU_CODIGO_HTTPS_NGROK/api/v1'}); export default Api; |
5 – Agora cole o seguinte código no arquivo ‘establishments_service.js’.
1 2 3 4 5 6 7 |
import Api from './api'; const EstablishmentService = { index: (latitude, longitude) => Api.get(`/google_stores?latitude=${latitude}&longitude=${longitude}`), } export default EstablishmentService; |
6 – Atualize o component APP para baixar os cafés e adicionar ao mapa os pins:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
... import EstablishmentService from './services/establishment_service'; ... const [locations, setLocations] = useState([]); ... useEffect(() => { ... loadCoffeShops(); }, []); ... // Load all coffee shops async function loadCoffeShops() { try { const response = await EstablishmentService.index(latitude, longitude); setLocations(response.data.results); } catch (error) { setLocations([]); } } ... latitude: (latitude != 0) ? latitude : 0, longitude: (longitude != 0) ? longitude : 0, latitudeDelta: 0.03, longitudeDelta: 0.03, } }> ... { locations.map(item => { return ( <Marker key={item.place_id} coordinate={ { latitude: item.geometry.location.lat, longitude: item.geometry.location.lng } } title={item.name} /> ) }) } ... </MapView> </View> ); }; ... |
8 – Visualize os marcadores nas cafeterias mais próximas \o/
Criando o Component de estabelecimento selecionado
1 – Atualize o establishment service colocando:
1 2 3 4 5 6 7 8 |
import Api from './api'; const EstablishmentService = { index: (latitude, longitude) => Api.get(`/google_stores?latitude=${latitude}&longitude=${longitude}`), show: (place_id) => Api.get(`/google_stores/${place_id}`) } export default EstablishmentsService; |
2 – Agora crie a pasta /src/components/Establishment.
3 – Dentro desse componente Establishment, crie o arquivo ‘index.js’ e cole o seguinte código nele:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import React, { useEffect, useState } from 'react'; import { StyleSheet, View, ScrollView, Dimensions } from 'react-native'; const Establishment = (props) => { return ( <ScrollView style={styles.container}> </ScrollView> ) } const styles = StyleSheet.create({ container: { position: 'absolute', top: 40, zIndex: 1, flex: 1, width: '80%', alignSelf: 'center' }, }); export default Establishment; |
4 – Vamos baixar os detalhes do café, no Component Establishment coloque:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
... import EstablishmentService from '../../services/google_establishment.js'; ... const Establishment = (props) => { const [establishment, setEstablishment] = useState(null); useEffect(() => { getEstablishmentInformations(); }, [props.place]); async function getEstablishmentInformations() { try { const response = await EstablishmentService.index(props.place.place_id); setEstablishment(response.data.result); } catch (error) { setEstablishment([]); } } ... |
5 – Agora vamos incluir as informações básicas do Establishment no Component e alguns estilos:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
import React, { useEffect, useState } from 'react'; import { StyleSheet, View, Text, Image, ScrollView, Button } from 'react-native'; ... ... const Establishment = (props) => { ... return ( <View style={styles.container}> { establishment != null && <View style={styles.background}> <ScrollView style={{height: 600}}> <View style={{ marginHorizontal: 30 }}> <View style={{alignSelf: 'flex-end'}}> <Button title="X" color="black" onPress={() => setEstablishment(null)} /> </View> { (establishment.photos) ? <Image style={styles.photo} source={{uri: `https://maps.googleapis.com/maps/api/place/photo?maxwidth=400&photoreference=${establishment.photos[0].photo_reference}&sensor=false&key=SUA_GOOGLE_KEY_API`}} alt="Store perfil"/> : <Image style={styles.photo} source={require('../../images/no_photo.jpg')} /> } <Text style={styles.title}>{props.place.name}</Text> </View> </ScrollView> </View> } </View> ) } const styles = StyleSheet.create({ container: { position: 'absolute', top: 40, zIndex: 1, flex: 1, width: '80%', alignSelf: 'center' }, background: { backgroundColor: 'black', paddingTop: 20, borderRadius: 20, }, photo: { height: 200, width: 200, }, title: { color: '#F56D50', fontSize: 17, marginTop: 10, }, }); export default Establishment; |
6 – Agora vamos setar o horário de funcionamento e endereço do nosso estabelecimento. Seu código ficará do seguinte jeito:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
... const Separator = () => ( <View style={styles.separator} /> ); const Establishment = (props) => { const [establishment, setEstablishment] = useState(null); ... return ( <View style={styles.container}> ... <Text style={styles.title}>{props.place.name}</Text> </View> { (establishment.opening_hours) ? <View> { (establishment.opening_hours.open_now === true) ? <Text style={{color: 'white', fontWeight: 'bold', marginTop: 10}}>Aberto</Text> : <Text style={{color: 'white', fontWeight: 'bold', marginTop: 10}}>Fechado</Text> } <Separator /> { establishment.opening_hours.weekday_text.map(schedule => { return ( <Text key={schedule} style={{color: 'white'}}>{schedule}</Text> ) }) } </View> : <View> <Separator /> <Text style={{color: 'white'}}>Não há cadastros de horário de funcionamento.</Text> </View> } <Separator /> <Text style={{color: 'white'}}>{establishment.formatted_address}</Text> <Separator /> </View> </ScrollView> </View> } </View> ) } ... separator: { marginVertical: 8, borderBottomColor: 'white', borderBottomWidth: StyleSheet.hairlineWidth, }, ... |
7 – Repare que no React Native não há <hr />. Para isso, foi necessário adicionar a classe <Separator />, que possui o mesmo comportamento.
8 – Depois de instalado, vamos adicionar nosso rodapé com ícones do Font Awesome. Adicione o seguinte no seu código:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
... <View style={styles.rodape}> <FontAwesomeIcon icon={faMapMarker} color='white' /> <Text style={{color: 'white', marginLeft: 10, fontSize: 11}}>Café selecionado</Text> </View> </View> ... const styles = StyleSheet.create({ ... rodape: { flexDirection: 'row', paddingLeft: 20, backgroundColor: '#393939', padding: 10, marginTop: 20, borderBottomLeftRadius: 20, borderBottomRightRadius: 20, }, |
9 – No component APP vamos importar o Establishment e também vamos preencher um café como selecionado quando ele for clicado:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
... import Establishment from './src/components/Establishment'; ... const App = () => { ... const [selected, setSelected] = useState(null); ... <View style={styles.container}> {(selected) && <Establishment place={selected} />} <MapView ... { locations.map(item => { return ( <Marker key={item.place_id} coordinate={ { latitude: item.geometry.location.lat, longitude: item.geometry.location.lng } } title={item.name} onPress={() => setSelected(item)} /> ) }) } </MapView> ... |
10 – Seu Component Establishment deve ficar:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
import React, { useEffect, useState } from 'react'; import { StyleSheet, View, Text, Image, ScrollView, Button } from 'react-native'; import EstablishmentService from '../../services/establishment_service.js'; const Separator = () => ( <View style={styles.separator} /> ); const Establishment = (props) => { const [establishment, setEstablishment] = useState(null); useEffect(() => { getEstablishmentInformations(); }, [props.place]); async function getEstablishmentInformations() { try { const response = await EstablishmentService.show(props.place.place_id); setEstablishment(response.data.result); } catch (error) { setEstablishment([]); } } return ( <View style={styles.container}> { establishment != null && <View style={styles.background}> <ScrollView style={{ height: 600 }}> <View style={{ marginHorizontal: 30 }}> <View style={{ alignSelf: 'flex-end' }}> <Button title="X" color="black" onPress={() => setEstablishment(null)} /> </View> { (establishment.photos) ? <Image style={styles.photo} source={ { uri: `https://maps.googleapis.com/maps/api/place/photo?maxwidth=400&photoreference=${establishment.photos[0].photo_reference}&sensor=false&key=<sua-key>` }} alt="Store perfil" /> : <Image style={styles.photo} source={require('../../images/no_photo.jpg')} /> } <Text style={styles.title}>{props.place.name}</Text> { (establishment.opening_hours) ? <View> <Text style={{ color: 'white', fontWeight: 'bold', marginTop: 10 }}> {(establishment.opening_hours.open_now === true) ? 'Aberto' : 'Fechado'} </Text> <Separator /> { establishment.opening_hours.weekday_text.map(schedule => { return ( <Text key={schedule} style={{ color: 'white' }}>{schedule}</Text> ) }) } </View> : <View> <Separator /> <Text style={{ color: 'white' }}>Não há cadastros de horário de funcionamento.</Text> </View> } <Separator /> <Text style={{ color: 'white' }}>{establishment.formatted_address}</Text> </View> </ScrollView> <View style={styles.rodape}> <Text style={{ color: 'white', marginLeft: 10, fontSize: 11 }}>Café selecionado</Text> </View> </View> } </View > ) } const styles = StyleSheet.create({ container: { position: 'absolute', top: 40, zIndex: 1, flex: 1, width: '80%', alignSelf: 'center' }, background: { backgroundColor: 'black', paddingTop: 20, borderRadius: 20, }, photo: { height: 200, width: 200, }, title: { color: '#F56D50', fontSize: 17, marginTop: 10, }, separator: { marginVertical: 8, borderBottomColor: 'white', borderBottomWidth: StyleSheet.hairlineWidth, }, rodape: { flexDirection: 'row', paddingLeft: 20, backgroundColor: '#393939', padding: 10, marginTop: 20, borderBottomLeftRadius: 20, borderBottomRightRadius: 20, }, }); export default Establishment; |
Puxando as avaliações, da nossa API
1 – Crie um service chamado ‘store.js’ e cole o seguinte código:
1 2 3 4 5 6 7 |
import Api from './api'; const StoreService = { show: (google_place_id) => Api.get(`/stores/${google_place_id}`) } export default StoreService; |
2 – Services criados!! Agora vá no componente /src/components/Establishment e crie a pasta ‘ListRatings’.
3 – Dentro da pasta ‘ListRatings’, crie o arquivo ‘index.js’ e cole o seguinte código nele:
1 2 3 4 5 6 7 8 9 10 11 12 |
import React, { useEffect, useState } from 'react'; import { View, Text, StyleSheet } from 'react-native'; const ListRatings = (props) => { return( <View> </View> ) } export default ListRatings; |
3 – Essa é a estrutura básica dele. Esse componente listará nossas avaliações pela API.
4 – Agora vamos puxar as informações de nossa API. Vamos conectar nosso componente ao service, armazenar os dados da resposta ao estado “ratings”, setar a quantidade de avaliações no código e o CSS do seguinte jeito:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
import React, { useEffect, useState } from 'react'; import { View, Text, StyleSheet } from 'react-native'; import StoreService from '../../../services/store'; const ListRatings = (props) => { const [store, setStore] = useState([]); useEffect(() => { getStore(); }, [props.place]); async function getStore() { try { const response = await StoreService.show(props.place.place_id); setStore(response.data); } catch (error) { setStore([]); } } return( <View> <View style={{flexDirection: 'row'}}> <Text style={styles.opinions}> { (store.ratings_count > 0) ? store.ratings_count : '0' } Opiniões </Text> </View> </View> ) } const styles = StyleSheet.create({ separator: { marginVertical: 8, borderBottomColor: 'white', borderBottomWidth: StyleSheet.hairlineWidth, marginHorizontal: 30, }, opinions: { color: 'white', marginLeft: 10, fontSize: 17, fontWeight: 'bold', marginRight: 20, }, }); export default ListRatings; |
5 – Agora, no arquivo /src/components/Establishment/index.js, coloque a seguinte importação e componente:
1 2 3 4 5 6 7 8 |
import ListRatings from './ListRatings'; ... <ListRatings place={props.place} /> </View> </ScrollView> ... |
6 – Agora, para continuar, precisaremos listar as estrelas das avaliações nos nossos componentes. Para isso, vamos instalar o componente Ratings do Materialize. Execute os seguintes comandos no seu terminal:
1 |
yarn add react-native-star-rating |
7 – Pronto! Agora, no componente ListRatings, adicione as estrelas do Materialize:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
... import StarRating from 'react-native-star-rating'; const ListRatings = (props) => { ... <View style={{flexDirection: 'row'}}> ... <StarRating disabled={true} maxStars={5} rating={store.ratings_average} fullStarColor="yellow" starSize={15} /> </View> </View> ) } ... |
8 – Agora vamos listar nossas avaliações. Substitua o código do componente ListRatings pelo código abaixo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
... const Separator = () => ( <View style={styles.separator} /> ); const ListRatings = (props) => { ... <StarRating disabled={true} maxStars={5} rating={store.ratings_average} fullStarColor="yellow" starSize={15} /> </View> { store.ratings_count > 0 && store.ratings.map((rating, index) => { return ( <View key={index}> <Separator /> <View style={{flexDirection: 'row', marginHorizontal: 20}}> <Text style={styles.user_name}>{ rating.user_name }</Text> <StarRating disabled={true} maxStars={5} rating={rating.value} fullStarColor="yellow" starSize={15} /> </View> <Text style={styles.text}>{ rating.opinion }</Text> <Text style={styles.text}>{ rating.date }</Text> </View> ) }) } </View> ) } ... user_name: { color: 'white', fontWeight: 'bold', marginRight: 30, }, text: { color: 'white', marginHorizontal: 20, fontSize: 10, }, ... |
9 – Seu component ListRating deve ficar:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
import React, { useEffect, useState } from 'react'; import { View, Text, StyleSheet } from 'react-native'; import StoreService from '../../../services/store_service'; import StarRating from 'react-native-star-rating'; const Separator = () => ( <View style={styles.separator} /> ); const ListRatings = (props) => { const [store, setStore] = useState([]); useEffect(() => { getStore(); }, [props.place]); async function getStore() { try { const response = await StoreService.show(props.place.place_id); setStore(response.data); } catch (error) { setStore([]); } } return ( <View> <View style={{ flexDirection: 'row' }}> <Text style={styles.opinions}> {(store.ratings_count > 0) ? store.ratings_count : '0'} Opiniões </Text> <StarRating disabled={true} maxStars={5} rating={store.ratings_average} fullStarColor="yellow" starSize={15} /> </View> { store.ratings_count > 0 && store.ratings.map((rating, index) => { return ( <View key={index}> <Separator /> <View style={{ flexDirection: 'row', marginHorizontal: 20 }}> <Text style={styles.user_name}>{rating.user_name}</Text> <StarRating disabled={true} maxStars={5} rating={rating.value} fullStarColor="yellow" starSize={15} /> </View> <Text style={styles.text}>{rating.opinion}</Text> <Text style={styles.text}>{rating.date}</Text> </View> ) }) } </View> ) } const styles = StyleSheet.create({ separator: { marginVertical: 8, borderBottomColor: 'white', borderBottomWidth: StyleSheet.hairlineWidth, marginHorizontal: 30, }, opinions: { color: 'white', marginLeft: 10, fontSize: 17, fontWeight: 'bold', marginRight: 20, }, user_name: { color: 'white', fontWeight: 'bold', marginRight: 30, }, text: { color: 'white', marginHorizontal: 20, fontSize: 10, }, }); export default ListRatings; |
10 – Seu component Establishment deve ficar:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
import React, { useEffect, useState } from 'react'; import { StyleSheet, View, Text, Image, ScrollView, Button } from 'react-native'; import EstablishmentService from '../../services/establishment_service.js'; import ListRatings from './ListRatings'; const Separator = () => ( <View style={styles.separator} /> ); const Establishment = (props) => { const [establishment, setEstablishment] = useState(null); useEffect(() => { getEstablishmentInformations(); }, [props.place]); async function getEstablishmentInformations() { try { const response = await EstablishmentService.show(props.place.place_id); setEstablishment(response.data.result); } catch (error) { setEstablishment([]); } } return ( <View style={styles.container}> { establishment != null && <View style={styles.background}> <ScrollView style={{ height: 600 }}> <View style={{ marginHorizontal: 30 }}> <View style={{ alignSelf: 'flex-end' }}> <Button title="X" color="black" onPress={() => setEstablishment(null)} /> </View> { (establishment.photos) ? <Image style={styles.photo} source={ { uri: `https://maps.googleapis.com/maps/api/place/photo?maxwidth=400&photoreference=${establishment.photos[0].photo_reference}&sensor=false&key=<sua-key>` }} alt="Store perfil" /> : <Image style={styles.photo} source={require('../../images/no_photo.jpg')} /> } <Text style={styles.title}>{props.place.name}</Text> { (establishment.opening_hours) ? <View> <Text style={{ color: 'white', fontWeight: 'bold', marginTop: 10 }}> {(establishment.opening_hours.open_now === true) ? 'Aberto' : 'Fechado'} </Text> <Separator /> { establishment.opening_hours.weekday_text.map(schedule => { return ( <Text key={schedule} style={{ color: 'white' }}>{schedule}</Text> ) }) } </View> : <View> <Separator /> <Text style={{ color: 'white' }}>Não há cadastros de horário de funcionamento.</Text> </View> } <Separator /> <Text style={{ color: 'white' }}>{establishment.formatted_address}</Text> <Separator /> <ListRatings place={props.place} /> </View> </ScrollView> <View style={styles.rodape}> <Text style={{ color: 'white', marginLeft: 10, fontSize: 11 }}>Café selecionado</Text> </View> </View> } </View > ) } const styles = StyleSheet.create({ container: { position: 'absolute', top: 40, zIndex: 1, flex: 1, width: '80%', alignSelf: 'center' }, background: { backgroundColor: 'black', paddingTop: 20, borderRadius: 20, }, photo: { height: 200, width: 200, }, title: { color: '#F56D50', fontSize: 17, marginTop: 10, }, separator: { marginVertical: 8, borderBottomColor: 'white', borderBottomWidth: StyleSheet.hairlineWidth, }, rodape: { flexDirection: 'row', paddingLeft: 20, backgroundColor: '#393939', padding: 10, marginTop: 20, borderBottomLeftRadius: 20, borderBottomRightRadius: 20, }, }); export default Establishment; |
Criando o Dropdown de cafés mais próximos
1 – Primeiro, vamos criar nosso service. Acesse a pasta /src/services/Local e crie o arquivo ‘store.js’.
2 – Atualize o service store:
1 2 3 4 5 6 7 8 |
import Api from './api'; const StoreService = { show: (google_place_id) => Api.get(`/stores/${google_place_id}`), index: (latitude, longitude) => Api.get('/stores', {params: {latitude: latitude, longitude: longitude}}), } export default StoreService; |
3 – Agora crie o componente /src/components/NearstCoffees.
4 – O componente NearstCoffees terá a seguinte estrutura básica:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import React, { useState } from 'react'; import { View, StyleSheet } from 'react-native'; const NearstCoffees = (props) => { return ( <View style={styles.container}> </View> ) } const styles = StyleSheet.create({ container: { position: 'absolute', top: 50, justifyContent: 'center', alignItems: 'center', zIndex: 2, flex: 1, width: 370, }, }); export default NearstCoffees; |
5 – Vamos instalar o FontAwesome para usarmos alguns ícones:
1 2 3 4 |
yarn add react-native-svg # ** yarn add @fortawesome/fontawesome-svg-core yarn add @fortawesome/free-solid-svg-icons yarn add @fortawesome/react-native-fontawesome |
6 – No component NearestCoffees inclua:
1 2 |
import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; import { faHeart, faAngleDown } from '@fortawesome/free-solid-svg-icons'; |
7 – Agora vamos incluir o dropdown para mostrar os cafés mais próximos:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
... import { Text, View, StyleSheet, TouchableOpacity } from 'react-native'; ... const NearstCoffees = (props) => { const [showDropdownButton, setShowDropdownButton] = useState(false); ... return ( <View style={styles.container}> <TouchableOpacity style={styles.button} onPress={() => (showDropdownButton == false) ? setShowDropdownButton(true) : setShowDropdownButton(false)}> <Text style={styles.text}>Find my Coffee</Text> <FontAwesomeIcon icon={faHeart} color='white' style={{marginRight: 5}} /> <FontAwesomeIcon icon={faAngleDown} color='white' /> </TouchableOpacity> { showDropdownButton == true && <View style={styles.nearstCoffees}> <Text style={styles.title}>Cafés mais amados próximos a você</Text> </View> } ... const styles = StyleSheet.create({ ... button: { height: 30, backgroundColor: 'black', borderRadius: 20, paddingLeft: 20, paddingRight: 20, justifyContent: 'space-between', flexDirection: 'row', display: 'flex', alignItems: 'center', }, text: { color: 'white', fontWeight: 'bold', marginRight: 20, }, nearstCoffees: { backgroundColor: 'black', width: 190, marginTop: 5, borderRadius: 5, padding: 10, }, title: { color: '#F56D50', fontWeight: 'bold', } |
9 – Vamos incluir um separador no component:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
... const Separator = () => ( <View style={styles.separator} /> ); ... const styles = StyleSheet.create({ ... separator: { marginVertical: 8, borderBottomColor: 'white', borderBottomWidth: StyleSheet.hairlineWidth, } }); |
10 – No component APP import o novo Component:
1 2 3 4 5 6 7 8 |
... import NearstCoffees from './src/components/NearstCoffees'; ... return ( <View style={styles.container}> <NearstCoffees latitude={latitude} longitude={longitude} /> ... |
11 – Seu component APP deve ficar:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
import React, {useState, useEffect} from 'react'; import { Alert, StyleSheet, View } from 'react-native'; import MapView, {Marker} from 'react-native-maps'; import * as Location from 'expo-location'; import Establishment from './src/components/Establishment'; import EstablishmentService from './src/services/establishment_service'; import NearstCoffees from './src/components/NearstCoffees'; export default function App() { const [latitude, setLatitude] = useState(0); const [longitude, setLongitude] = useState(0); const [locations, setLocations] = useState([]); const [selected, setSelected] = useState(null); useEffect(() => { (async () => { let {status} = await Location.requestPermissionsAsync(); if(status !== 'granted') { Alert.alert('Ative as permissões de uso do GPS para acessar o APP') } else { let location = await Location.getCurrentPositionAsync({}); setLatitude(location.coords.latitude); setLongitude(location.coords.longitude); } })(); loadCoffees(); }, []); async function loadCoffees() { try { const response = await EstablishmentService.index(latitude, longitude); setLocations(response.data.results); } catch (error) { setLocations([]); } } return ( <View style={styles.container}> <NearstCoffees latitude={latitude} longitude={longitude} /> {(selected) && <Establishment place={selected} />} <MapView style={styles.map} region={ { latitude: latitude, longitude: longitude, latitudeDelta: 0.03, longitudeDelta: 0.03 } } > <Marker title='Seu Local' icon={require('./src/images/my-location-pin.png')} coordinate={ { latitude: latitude, longitude: longitude } } /> { locations.map(item => { return ( <Marker key={item.place_id} title={item.name} icon={require('./src/images/coffee-big-pin.png')} coordinate={ { latitude: item.geometry.location.lat, longitude: item.geometry.location.lng } } onPress={() => setSelected(item)} /> ) }) } </MapView> </View> ); } const styles = StyleSheet.create({ container: { flex: 1, zIndex: 0 }, map: { height: '100%', width: '100%' } }); |
12 – Seu Component NearestCoffees deve ficar:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
import React, { useState } from 'react'; import { Text, View, StyleSheet, TouchableOpacity } from 'react-native'; import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; import { faHeart, faAngleDown } from '@fortawesome/free-solid-svg-icons'; const Separator = () => ( <View style={styles.separator} /> ); const NearstCoffees = (props) => { const [showDropdownButton, setShowDropdownButton] = useState(false); return ( <View style={styles.container}> <TouchableOpacity style={styles.button} onPress={() => (setShowDropdownButton(!showDropdownButton))}> <Text style={styles.text}>Find my Coffee</Text> <FontAwesomeIcon icon={faHeart} color='white' style={{ marginRight: 5 }} /> <FontAwesomeIcon icon={faAngleDown} color='white' /> </TouchableOpacity> { showDropdownButton == true && <View style={styles.nearstCoffees}> <Text style={styles.title}>Cafés mais amados próximos a você</Text> </View> } </View> ) } const styles = StyleSheet.create({ container: { position: 'absolute', top: 50, justifyContent: 'center', alignItems: 'center', zIndex: 2, flex: 1, width: 370, }, button: { height: 30, backgroundColor: 'black', borderRadius: 20, paddingLeft: 20, paddingRight: 20, justifyContent: 'space-between', flexDirection: 'row', display: 'flex', alignItems: 'center', }, text: { color: 'white', fontWeight: 'bold', marginRight: 20, }, nearstCoffees: { backgroundColor: 'black', width: 190, marginTop: 5, borderRadius: 5, padding: 10, }, title: { color: '#F56D50', fontWeight: 'bold', }, separator: { marginVertical: 8, borderBottomColor: 'white', borderBottomWidth: StyleSheet.hairlineWidth, } }); export default NearstCoffees; |
Listando os cafés mais próximos
1 – Crie o component ListCoffees dentro da pasta /src/components/NearestCoffees com o arquivo index.js. 2 – Coloque a estrutura básica nele:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import React, { useEffect, useState } from 'react'; import { View, Text, StyleSheet, ScrollView } from 'react-native'; const ListCoffees = (props) => { return( <ScrollView style={styles.container}> </ScrollView> ) } const styles = StyleSheet.create({ container: { height: 300, }, }); export default ListCoffees; |
3 – Vamos baixar os cafés próximos colocando nele:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
... import StoreService from '../../../services/store.js'; ... const ListCoffees = (props) => { const [stores, setStores] = useState([]); useEffect(() => { loadNearstStores(); }, []); async function loadNearstStores() { try { const response = await StoreService.index(props.latitude, props.longitude); setStores(response.data); } catch (error) { setStores([]); } } ... |
4 – Vamos incluir o separador e o StartRating:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
... import StarRating from 'react-native-star-rating'; const Separator = () => ( <View style={styles.separator} /> ); ... const styles = StyleSheet.create({ ... separator: { marginVertical: 8, borderBottomColor: 'white', borderBottomWidth: StyleSheet.hairlineWidth, }, }); |
5 – Agora vamos exibir os reviews, inclua no component:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
... <ScrollView style={styles.container}> { stores.map((store, index) => { return ( <View style={{ flex: 1 }} key={index}> <Text style={styles.store_name}>{store.name}</Text> <Text style={styles.store_address}> {store.address} </Text> <View style={{ flexDirection: 'row' }}> <StarRating disabled={true} maxStars={5} rating={store.ratings_average} fullStarColor="yellow" starSize={15} /> <Text style={{ color: 'white', marginLeft: 10, fontSize: 10 }}> {store.ratings_count} Opiniões </Text> </View> <Separator /> </View> ) }) } </ScrollView> ... const styles = StyleSheet.create({ ... store_name: { color: 'white', fontWeight: 'bold', fontSize: 10, }, store_address: { color: 'white', fontSize: 9, }, }) |
5 – Finalmente vamos incluir o ListCoffees no component NearestCoffees, coloque:
1 2 3 4 5 6 7 8 9 10 |
... import ListCoffees from './ListCoffees'; ... <View style={styles.nearstCoffees}> <Text style={styles.title}>Cafés mais amados próximos a você</Text> <Separator /> <ListCoffees latitude={props.latitude} longitude={props.longitude} /> </View> |
6 – Seu component ListCoffees deve ficar:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
import React, { useState } from 'react'; import { Text, View, StyleSheet, TouchableOpacity } from 'react-native'; import { FontAwesomeIcon } from '@fortawesome/react-native-fontawesome'; import { faHeart, faAngleDown } from '@fortawesome/free-solid-svg-icons'; import ListCoffees from './ListCoffees'; const Separator = () => ( <View style={styles.separator} /> ); const NearstCoffees = (props) => { const [showDropdownButton, setShowDropdownButton] = useState(false); return ( <View style={styles.container}> <TouchableOpacity style={styles.button} onPress={() => (setShowDropdownButton(!showDropdownButton))}> <Text style={styles.text}>Find my Coffee</Text> <FontAwesomeIcon icon={faHeart} color='white' style={{ marginRight: 5 }} /> <FontAwesomeIcon icon={faAngleDown} color='white' /> </TouchableOpacity> { showDropdownButton == true && <View style={styles.nearstCoffees}> <Text style={styles.title}>Cafés mais amados próximos a você</Text> <Separator /> <ListCoffees latitude={props.latitude} longitude={props.longitude} /> </View> } </View> ) } const styles = StyleSheet.create({ container: { position: 'absolute', top: 50, justifyContent: 'center', alignItems: 'center', zIndex: 2, flex: 1, width: 370, }, button: { height: 30, backgroundColor: 'black', borderRadius: 20, paddingLeft: 20, paddingRight: 20, justifyContent: 'space-between', flexDirection: 'row', display: 'flex', alignItems: 'center', }, text: { color: 'white', fontWeight: 'bold', marginRight: 20, }, nearstCoffees: { backgroundColor: 'black', width: 190, marginTop: 5, borderRadius: 5, padding: 10, }, title: { color: '#F56D50', fontWeight: 'bold', }, separator: { marginVertical: 8, borderBottomColor: 'white', borderBottomWidth: StyleSheet.hairlineWidth, } }); export default NearstCoffees; |
7 – Seu component ListCoffees deve ficar:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
import React, { useEffect, useState } from 'react'; import { View, Text, StyleSheet, ScrollView } from 'react-native'; import StoreService from '../../../services/store_service.js'; import StarRating from 'react-native-star-rating'; const Separator = () => ( <View style={styles.separator} /> ); const ListCoffees = (props) => { const [stores, setStores] = useState([]); useEffect(() => { loadNearstStores(); }, []); async function loadNearstStores() { try { const response = await StoreService.index(props.latitude, props.longitude); setStores(response.data); } catch (error) { setStores([]); } } return ( <ScrollView style={styles.container}> { stores.map((store, index) => { return ( <View style={{ flex: 1 }} key={index}> <Text style={styles.store_name}>{store.name}</Text> <Text style={styles.store_address}> {store.address} </Text> <View style={{ flexDirection: 'row' }}> <StarRating disabled={true} maxStars={5} rating={store.ratings_average} fullStarColor="yellow" starSize={15} /> <Text style={{ color: 'white', marginLeft: 10, fontSize: 10 }}> {store.ratings_count} Opiniões </Text> </View> <Separator /> </View> ) }) } </ScrollView> ) } const styles = StyleSheet.create({ container: { height: 300, }, separator: { marginVertical: 8, borderBottomColor: 'white', borderBottomWidth: StyleSheet.hairlineWidth, }, store_name: { color: 'white', fontWeight: 'bold', fontSize: 10, }, store_address: { color: 'white', fontSize: 9, }, }); export default ListCoffees; |
8 – Suba o projeto e teste rodando:
1 |
expo start |