├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json └── src ├── App.js ├── assets └── airbnb-logo.svg ├── index.js ├── pages ├── AddProperty │ ├── index.js │ └── styles.js ├── App │ ├── components │ │ ├── Button │ │ │ ├── index.js │ │ │ └── styles.js │ │ └── Properties │ │ │ ├── index.js │ │ │ └── styles.js │ ├── index.js │ └── styles.js ├── Property │ ├── index.js │ └── styles.js ├── SignIn │ ├── index.js │ └── styles.js └── SignUp │ ├── index.js │ └── styles.js ├── routes.js ├── services ├── api.js └── auth.js └── styles └── global.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocketseat-content/blog-adonis-reactjs-react-native-airbnb-web/66dda7042f3cc9db343df6ae7a3f48b832064e24/README.md -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "airbnb-web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.18.0", 7 | "classnames": "^2.2.6", 8 | "font-awesome": "^4.7.0", 9 | "polished": "^1.9.3", 10 | "prop-types": "^15.6.2", 11 | "react": "^16.4.2", 12 | "react-dimensions": "^1.3.1", 13 | "react-dom": "^16.4.2", 14 | "react-dropzone": "^5.0.1", 15 | "react-map-gl": "^3.3.4", 16 | "react-router-dom": "^4.3.1", 17 | "react-router-modal": "^1.4.1", 18 | "react-scripts": "1.1.4", 19 | "stringquery": "^1.0.8", 20 | "styled-components": "^3.4.2" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test --env=jsdom", 26 | "eject": "react-scripts eject" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rocketseat-content/blog-adonis-reactjs-react-native-airbnb-web/66dda7042f3cc9db343df6ae7a3f48b832064e24/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Routes from "./routes"; 3 | import "./styles/global"; 4 | 5 | const App = () => ; 6 | export default App; 7 | -------------------------------------------------------------------------------- /src/assets/airbnb-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | ReactDOM.render(, document.getElementById('root')); -------------------------------------------------------------------------------- /src/pages/AddProperty/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { withRouter } from "react-router-dom"; 3 | import querySearch from "stringquery"; 4 | import classNames from "classnames"; 5 | import PropTypes from "prop-types"; 6 | 7 | import { Form, File } from "./styles"; 8 | 9 | import api from "../../services/api"; 10 | 11 | class AddProperty extends Component { 12 | static propTypes = { 13 | location: PropTypes.shape({ 14 | search: PropTypes.string 15 | }).isRequired, 16 | history: PropTypes.shape({ 17 | push: PropTypes.func 18 | }).isRequired 19 | }; 20 | 21 | state = { 22 | title: "", 23 | address: "", 24 | price: "", 25 | error: "", 26 | files: [] 27 | }; 28 | 29 | componentDidMount() { 30 | const params = querySearch(this.props.location.search); 31 | if ( 32 | !params.hasOwnProperty("latitude") || 33 | !params.hasOwnProperty("longitude") 34 | ) { 35 | alert("É necessário definir a latitude e longitude para um imóvel."); 36 | this.props.history.push("/app"); 37 | } 38 | 39 | this.setState({ ...params }); 40 | } 41 | 42 | handleDrop = files => this.setState({ files }); 43 | 44 | renderFiles() { 45 | const { files } = this.state; 46 | return !files.length ? ( 47 |

Jogue as imagens ou clique aqui para adiciona-las

48 | ) : ( 49 | files.map(file => ) 50 | ); 51 | } 52 | 53 | handleSubmit = async e => { 54 | e.preventDefault(); 55 | 56 | try { 57 | const { title, address, price, latitude, longitude, files } = this.state; 58 | 59 | if (!title || !address || !price || !latitude || !longitude) { 60 | this.setState({ error: "Preencha todos os campos" }); 61 | return; 62 | } 63 | 64 | const { 65 | data: { id } 66 | } = await api.post("/properties", { 67 | title, 68 | address, 69 | price, 70 | latitude, 71 | longitude 72 | }); 73 | 74 | if (!files.length) this.props.history.push("/app"); 75 | 76 | const data = new FormData(); 77 | files.map((file, index) => 78 | data.append(`image[${index}]`, file, file.name) 79 | ); 80 | 81 | const config = { 82 | headers: { 83 | "content-type": "multipart/form-data" 84 | } 85 | }; 86 | 87 | await api.post(`/properties/${id}/images`, data, config); 88 | 89 | this.props.history.push("/app"); 90 | } catch (err) { 91 | this.setState({ error: "Ocorreu algum erro ao adicionar o imóvel" }); 92 | } 93 | }; 94 | 95 | handleCancel = e => { 96 | e.preventDefault(); 97 | 98 | this.props.history.push("/app"); 99 | }; 100 | 101 | render() { 102 | return ( 103 |
104 |

Adicionar imóvel

105 |
106 | {this.state.error &&

{this.state.error}

} 107 | this.setState({ title: e.target.value })} 111 | /> 112 | this.setState({ address: e.target.value })} 116 | /> 117 | this.setState({ price: e.target.value })} 121 | /> 122 | 127 | {this.renderFiles()} 128 | 129 |
130 | 131 | 134 |
135 |
136 | ); 137 | } 138 | } 139 | 140 | export default withRouter(AddProperty); 141 | -------------------------------------------------------------------------------- /src/pages/AddProperty/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import Dropzone from "react-dropzone"; 3 | 4 | export const File = styled(Dropzone)` 5 | border: 2px dashed #ff3333; 6 | width: 100%; 7 | max-width: 660px; 8 | font-size: 16px; 9 | color: #777777; 10 | text-align: center; 11 | display: grid; 12 | grid-template-columns: 100px 100px 100px; 13 | grid-gap: 5px; 14 | background-color: #fff; 15 | color: #444; 16 | &.without-files { 17 | display: flex; 18 | } 19 | img { 20 | width: 100px; 21 | } 22 | p { 23 | margin-top: 15px; 24 | border: none !important; 25 | } 26 | `; 27 | 28 | export const Form = styled.form` 29 | display: flex; 30 | flex-direction: column; 31 | align-items: center; 32 | padding: 10px 20px; 33 | p { 34 | color: #ff3333; 35 | margin-bottom: 15px; 36 | border: 1px solid #ff3333; 37 | padding: 10px; 38 | width: 100%; 39 | text-align: center; 40 | } 41 | input { 42 | flex: 1; 43 | height: 46px; 44 | margin-bottom: 15px; 45 | padding: 0 20px; 46 | color: #777777; 47 | font-size: 15px; 48 | width: 100%; 49 | border: 1px solid #cccccc; 50 | &::placeholder { 51 | color: #999999; 52 | } 53 | } 54 | hr { 55 | margin: 20px 0; 56 | border: none; 57 | border-bottom: 1px solid #cdcdcd; 58 | width: 100%; 59 | } 60 | a { 61 | font-size: 16px; 62 | font-weight: bold; 63 | color: #999999; 64 | text-decoration: none; 65 | } 66 | div.actions { 67 | display: flex; 68 | margin-top: 15px; 69 | width: 100%; 70 | justify-content: space-between; 71 | button { 72 | color: #ffffff; 73 | font-size: 16px; 74 | background: #fc6963; 75 | height: 56px; 76 | border: 0; 77 | border-radius: 5px; 78 | padding: 0 30px; 79 | &.cancel { 80 | background: #222222; 81 | } 82 | } 83 | } 84 | `; 85 | -------------------------------------------------------------------------------- /src/pages/App/components/Button/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | 4 | import { Button } from "./styles"; 5 | 6 | const CustomButton = ({ children, color, ...props }) => ( 7 | 10 | ); 11 | 12 | CustomButton.propTypes = { 13 | children: PropTypes.element.isRequired, 14 | color: PropTypes.string.isRequired, 15 | props: PropTypes.object 16 | }; 17 | 18 | export default CustomButton; 19 | -------------------------------------------------------------------------------- /src/pages/App/components/Button/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { darken } from "polished"; 3 | 4 | export const Button = styled.button` 5 | width: 60px; 6 | height: 60px; 7 | border-radius: 60px; 8 | border: none; 9 | background-color: ${({ color }) => color}; 10 | margin-top: 10px; 11 | color: #fff; 12 | i { 13 | font-size: 18px; 14 | } 15 | &:hover { 16 | background-color: ${({ color }) => darken(0.05, color)}; 17 | } 18 | &:active { 19 | background-color: ${({ color }) => darken(0.07, color)}; 20 | } 21 | `; 22 | -------------------------------------------------------------------------------- /src/pages/App/components/Properties/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Marker } from "react-map-gl"; 3 | import { Link } from "react-router-dom"; 4 | import PropTypes from "prop-types"; 5 | 6 | import { Pin } from "./styles"; 7 | 8 | const intlMonetary = new Intl.NumberFormat("pt-BR", { 9 | style: "currency", 10 | currency: "BRL", 11 | minimumFractionDigits: 2 12 | }); 13 | 14 | const Properties = ({ properties, match }) => 15 | properties.map(property => ( 16 | 21 | 22 | 23 | {intlMonetary.format(property.price)} 24 | 25 | 26 | 27 | )); 28 | 29 | Properties.propTypes = { 30 | properties: PropTypes.arrayOf( 31 | PropTypes.shape({ 32 | id: PropTypes.number, 33 | title: PropTypes.string, 34 | price: PropTypes.number, 35 | longitude: PropTypes.number, 36 | latitude: PropTypes.number 37 | }) 38 | ).isRequired, 39 | match: PropTypes.shape({ 40 | url: PropTypes.string 41 | }).isRequired 42 | }; 43 | 44 | export default Properties; 45 | -------------------------------------------------------------------------------- /src/pages/App/components/Properties/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | export const Pin = styled.div` 3 | padding: 5px; 4 | background: #fc6963; 5 | border: 0; 6 | border-radius: 5px; 7 | a { 8 | color: #fff; 9 | font-size: 14px; 10 | text-decoration: none; 11 | } 12 | `; 13 | -------------------------------------------------------------------------------- /src/pages/App/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from "react"; 2 | import Dimensions from "react-dimensions"; 3 | import { withRouter } from "react-router-dom"; 4 | import { ModalRoute } from "react-router-modal"; 5 | import MapGL from "react-map-gl"; 6 | import PropTypes from "prop-types"; 7 | import debounce from "lodash/debounce"; 8 | 9 | import api from "../../services/api"; 10 | import { logout } from "../../services/auth"; 11 | 12 | import Properties from "./components/Properties"; 13 | import Button from "./components/Button"; 14 | 15 | import AddProperty from "../AddProperty"; 16 | import Property from "../Property"; 17 | 18 | import { Container, ButtonContainer, PointReference } from "./styles"; 19 | 20 | const TOKEN = 21 | "pk.eyJ1IjoiaGlnb3JvY2tldCIsImEiOiJjamlrdWJuY3gyaHYxM3Bvbmg0cGRwY3R0In0._TdjX9rYrjZ6Q6FFXOGwsQ"; 22 | 23 | class Map extends Component { 24 | static propTypes = { 25 | containerWidth: PropTypes.number.isRequired, 26 | containerHeight: PropTypes.number.isRequired 27 | }; 28 | 29 | constructor() { 30 | super(); 31 | this.updatePropertiesLocalization = debounce( 32 | this.updatePropertiesLocalization, 33 | 500 34 | ); 35 | } 36 | 37 | state = { 38 | viewport: { 39 | latitude: -27.2108001, 40 | longitude: -49.6446024, 41 | zoom: 12.8, 42 | bearing: 0, 43 | pitch: 0 44 | }, 45 | properties: [], 46 | addActivate: false 47 | }; 48 | 49 | componentDidMount() { 50 | this.loadProperties(); 51 | } 52 | 53 | updatePropertiesLocalization() { 54 | this.loadProperties(); 55 | } 56 | 57 | loadProperties = async () => { 58 | const { latitude, longitude } = this.state.viewport; 59 | try { 60 | const response = await api.get("/properties", { 61 | params: { latitude, longitude } 62 | }); 63 | this.setState({ properties: response.data }); 64 | } catch (err) { 65 | console.log(err); 66 | } 67 | }; 68 | 69 | handleLogout = e => { 70 | logout(); 71 | this.props.history.push("/"); 72 | }; 73 | 74 | handleAddProperty = () => { 75 | const { match, history } = this.props; 76 | const { latitude, longitude } = this.state.viewport; 77 | history.push( 78 | `${match.url}/properties/add?latitude=${latitude}&longitude=${longitude}` 79 | ); 80 | 81 | this.setState({ addActivate: false }); 82 | }; 83 | 84 | renderActions() { 85 | return ( 86 | 87 | 93 | 96 | 97 | ); 98 | } 99 | 100 | renderButtonAdd() { 101 | return ( 102 | this.state.addActivate && ( 103 | 104 | 105 |
106 | 109 | 115 |
116 |
117 | ) 118 | ); 119 | } 120 | 121 | render() { 122 | const { 123 | containerWidth: width, 124 | containerHeight: height, 125 | match 126 | } = this.props; 127 | const { properties, addActivate } = this.state; 128 | return ( 129 | 130 | this.setState({ viewport })} 137 | onViewStateChange={this.updatePropertiesLocalization.bind(this)} 138 | > 139 | {!addActivate && } 140 | 141 | {this.renderButtonAdd()} 142 | {this.renderActions()} 143 | 148 | 153 | 154 | ); 155 | } 156 | } 157 | 158 | const DimensionedMap = withRouter(Dimensions()(Map)); 159 | const App = () => ( 160 | 161 | 162 | 163 | ); 164 | 165 | export default App; 166 | -------------------------------------------------------------------------------- /src/pages/App/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { darken } from "polished"; 3 | 4 | export const Container = styled.div` 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | height: 100vh; 9 | `; 10 | 11 | export const ButtonContainer = styled.div` 12 | position: absolute; 13 | bottom: 20px; 14 | right: 10px; 15 | display: flex; 16 | flex-direction: column; 17 | `; 18 | 19 | export const PointReference = styled.div` 20 | position: absolute; 21 | top: 0; 22 | width: 100vw; 23 | height: 100vh; 24 | pointer-events: none; 25 | display: flex; 26 | align-items: center; 27 | justify-content: center; 28 | flex-direction: column; 29 | i { 30 | color: #fc6963; 31 | pointer-events: all; 32 | font-size: 50px; 33 | margin-top: 112px; 34 | margin-left: 12px; 35 | -webkit-text-fill-color: #fc6963; 36 | -webkit-text-stroke-width: 2px; 37 | -webkit-text-stroke-color: ${() => darken(0.05, "#fc6963")}; 38 | } 39 | div { 40 | margin-top: 100px; 41 | button { 42 | border: none; 43 | font-size: 15px; 44 | height: 46px; 45 | margin: 0 10px; 46 | background-color: #fc6963; 47 | color: #ffffff; 48 | padding: 0 20px; 49 | border-radius: 3px; 50 | pointer-events: all; 51 | text-align: center; 52 | &.cancel { 53 | background: #ffffff; 54 | color: #333333; 55 | } 56 | } 57 | } 58 | `; -------------------------------------------------------------------------------- /src/pages/Property/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component, Fragment } from "react"; 2 | import { Container, Images } from "./styles"; 3 | import PropTypes from "prop-types"; 4 | 5 | import api from "../../services/api"; 6 | 7 | const intlMonetary = new Intl.NumberFormat("pt-BR", { 8 | style: "currency", 9 | currency: "BRL", 10 | minimumFractionDigits: 2 11 | }); 12 | 13 | export default class Property extends Component { 14 | static propTypes = { 15 | match: PropTypes.shape({ 16 | params: PropTypes.shape({ 17 | id: PropTypes.string 18 | }) 19 | }).isRequired 20 | }; 21 | state = { 22 | property: null, 23 | loading: false 24 | }; 25 | 26 | async componentDidMount() { 27 | try { 28 | const { id } = this.props.match.params; 29 | this.setState({ loading: true }); 30 | 31 | const { data } = await api.get(`/properties/${id}`); 32 | this.setState({ property: data }); 33 | } catch (err) { 34 | console.log(err); 35 | } finally { 36 | this.setState({ loading: false }); 37 | } 38 | } 39 | renderProperty() { 40 | const { property } = this.state; 41 | 42 | if (!property) { 43 | return "Imóvel não encontrado!"; 44 | } 45 | 46 | return ( 47 | 48 |

{property.title}

49 |
50 |

{property.address}

51 | 52 | {property.images.map(image => ( 53 | {image.path} 54 | ))} 55 | 56 | {intlMonetary.format(property.price)} 57 |
58 | ); 59 | } 60 | 61 | render() { 62 | const { loading } = this.state; 63 | return ( 64 | 65 | {loading ?

Carregando

: this.renderProperty()} 66 |
67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/pages/Property/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Images = styled.div` 4 | width: 100%; 5 | max-width: 660px; 6 | font-size: 16px; 7 | color: #777777; 8 | text-align: center; 9 | display: grid; 10 | grid-template-columns: 100px 100px 100px; 11 | grid-gap: 5px; 12 | background-color: #fff; 13 | color: #444; 14 | &.without-images { 15 | display: flex; 16 | } 17 | img { 18 | width: 100px; 19 | } 20 | p { 21 | margin-top: 15px; 22 | border: none !important; 23 | } 24 | `; 25 | 26 | export const Container = styled.div` 27 | display: flex; 28 | flex-direction: column; 29 | align-items: center; 30 | padding: 10px 20px; 31 | p { 32 | margin-bottom: 15px; 33 | padding: 10px; 34 | width: 100%; 35 | } 36 | hr { 37 | margin: 20px 0; 38 | border: none; 39 | border-bottom: 1px solid #cdcdcd; 40 | width: 100%; 41 | } 42 | 43 | span { 44 | color: #fc6963; 45 | font-size: 20px; 46 | } 47 | `; 48 | -------------------------------------------------------------------------------- /src/pages/SignIn/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Link, withRouter } from "react-router-dom"; 3 | 4 | import Logo from "../../assets/airbnb-logo.svg"; 5 | import api from "../../services/api"; 6 | import { login } from "../../services/auth"; 7 | 8 | import { Form, Container } from "./styles"; 9 | 10 | class SignIn extends Component { 11 | state = { 12 | email: "", 13 | password: "", 14 | error: "" 15 | }; 16 | 17 | handleSignIn = async e => { 18 | e.preventDefault(); 19 | const { email, password } = this.state; 20 | if (!email || !password) { 21 | this.setState({ error: "Preencha e-mail e senha para continuar!" }); 22 | } else { 23 | try { 24 | const response = await api.post("/sessions", { email, password }); 25 | login(response.data.token); 26 | this.props.history.push("/app"); 27 | } catch (err) { 28 | this.setState({ 29 | error: 30 | "Houve um problema com o login, verifique suas credenciais. T.T" 31 | }); 32 | } 33 | } 34 | }; 35 | 36 | render() { 37 | return ( 38 | 39 |
40 | Airbnb logo 41 | {this.state.error &&

{this.state.error}

} 42 | this.setState({ email: e.target.value })} 46 | /> 47 | this.setState({ password: e.target.value })} 51 | /> 52 | 53 |
54 | Criar conta grátis 55 |
56 |
57 | ); 58 | } 59 | } 60 | 61 | export default withRouter(SignIn); 62 | -------------------------------------------------------------------------------- /src/pages/SignIn/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | height: 100vh; 8 | `; 9 | 10 | export const Form = styled.form` 11 | width: 400px; 12 | background: #fff; 13 | padding: 20px; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | img { 18 | width: 100px; 19 | margin: 10px 0 40px; 20 | } 21 | p { 22 | color: #ff3333; 23 | margin-bottom: 15px; 24 | border: 1px solid #ff3333; 25 | padding: 10px; 26 | width: 100%; 27 | text-align: center; 28 | } 29 | input { 30 | flex: 1; 31 | height: 46px; 32 | margin-bottom: 15px; 33 | padding: 0 20px; 34 | color: #777; 35 | font-size: 15px; 36 | width: 100%; 37 | border: 1px solid #ddd; 38 | &::placeholder { 39 | color: #999; 40 | } 41 | } 42 | button { 43 | color: #fff; 44 | font-size: 16px; 45 | background: #fc6963; 46 | height: 56px; 47 | border: 0; 48 | border-radius: 5px; 49 | width: 100%; 50 | } 51 | hr { 52 | margin: 20px 0; 53 | border: none; 54 | border-bottom: 1px solid #cdcdcd; 55 | width: 100%; 56 | } 57 | a { 58 | font-size: 16; 59 | font-weight: bold; 60 | color: #999; 61 | text-decoration: none; 62 | } 63 | `; 64 | -------------------------------------------------------------------------------- /src/pages/SignUp/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import { Link, withRouter } from "react-router-dom"; 3 | 4 | import Logo from "../../assets/airbnb-logo.svg"; 5 | 6 | import api from "../../services/api"; 7 | 8 | import { Form, Container } from "./styles"; 9 | 10 | class SignUp extends Component { 11 | state = { 12 | username: "", 13 | email: "", 14 | password: "", 15 | error: "" 16 | }; 17 | 18 | handleSignUp = async e => { 19 | e.preventDefault(); 20 | const { username, email, password } = this.state; 21 | if (!username || !email || !password) { 22 | this.setState({ error: "Preencha todos os dados para se cadastrar" }); 23 | } else { 24 | try { 25 | await api.post("/users", { username, email, password }); 26 | this.props.history.push("/"); 27 | } catch (err) { 28 | console.log(err); 29 | this.setState({ error: "Ocorreu um erro ao registrar sua conta. T.T" }); 30 | } 31 | } 32 | }; 33 | 34 | render() { 35 | return ( 36 | 37 |
38 | Airbnb logo 39 | {this.state.error &&

{this.state.error}

} 40 | this.setState({ username: e.target.value })} 44 | /> 45 | this.setState({ email: e.target.value })} 49 | /> 50 | this.setState({ password: e.target.value })} 54 | /> 55 | 56 |
57 | Fazer login 58 |
59 |
60 | ); 61 | } 62 | } 63 | 64 | export default withRouter(SignUp); 65 | -------------------------------------------------------------------------------- /src/pages/SignUp/styles.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const Container = styled.div` 4 | display: flex; 5 | align-items: center; 6 | justify-content: center; 7 | height: 100vh; 8 | `; 9 | 10 | export const Form = styled.form` 11 | width: 400px; 12 | background: #fff; 13 | padding: 20px; 14 | display: flex; 15 | flex-direction: column; 16 | align-items: center; 17 | img { 18 | width: 100px; 19 | margin: 10px 0 40px; 20 | } 21 | p { 22 | color: #ff3333; 23 | margin-bottom: 15px; 24 | border: 1px solid #ff3333; 25 | padding: 10px; 26 | width: 100%; 27 | text-align: center; 28 | } 29 | input { 30 | flex: 1; 31 | height: 46px; 32 | margin-bottom: 15px; 33 | padding: 0 20px; 34 | color: #777; 35 | font-size: 15px; 36 | width: 100%; 37 | border: 1px solid #ddd; 38 | &::placeholder { 39 | color: #999; 40 | } 41 | } 42 | button { 43 | color: #fff; 44 | font-size: 16px; 45 | background: #fc6963; 46 | height: 56px; 47 | border: 0; 48 | border-radius: 5px; 49 | width: 100%; 50 | } 51 | hr { 52 | margin: 20px 0; 53 | border: none; 54 | border-bottom: 1px solid #cdcdcd; 55 | width: 100%; 56 | } 57 | a { 58 | font-size: 16; 59 | font-weight: bold; 60 | color: #999; 61 | text-decoration: none; 62 | } 63 | `; 64 | -------------------------------------------------------------------------------- /src/routes.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from "react"; 2 | import { BrowserRouter, Route, Switch, Redirect } from "react-router-dom"; 3 | import { ModalContainer } from "react-router-modal"; 4 | import "react-router-modal/css/react-router-modal.css"; 5 | 6 | import { isAuthenticated } from "./services/auth"; 7 | 8 | import SignUp from "./pages/SignUp"; 9 | import SignIn from "./pages/SignIn"; 10 | import App from "./pages/App"; 11 | 12 | const PrivateRoute = ({ component: Component, ...rest }) => ( 13 | 16 | isAuthenticated() ? ( 17 | 18 | ) : ( 19 | 20 | ) 21 | } 22 | /> 23 | ); 24 | 25 | const Routes = () => ( 26 | 27 | 28 | 29 | 30 | 31 | 32 |

Page not found

} /> 33 |
34 | 35 |
36 |
37 | ); 38 | 39 | export default Routes; 40 | -------------------------------------------------------------------------------- /src/services/api.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { getToken } from "./auth"; 3 | 4 | const api = axios.create({ 5 | baseURL: "http://127.0.0.1:3333" 6 | }); 7 | 8 | api.interceptors.request.use(async config => { 9 | const token = getToken(); 10 | if (token) { 11 | config.headers.Authorization = `Bearer ${token}`; 12 | } 13 | return config; 14 | }); 15 | 16 | export default api; -------------------------------------------------------------------------------- /src/services/auth.js: -------------------------------------------------------------------------------- 1 | export const TOKEN_KEY = "@airbnb-Token"; 2 | export const isAuthenticated = () => localStorage.getItem(TOKEN_KEY) !== null; 3 | export const getToken = () => localStorage.getItem(TOKEN_KEY); 4 | export const login = token => { 5 | localStorage.setItem(TOKEN_KEY, token); 6 | }; 7 | export const logout = () => { 8 | localStorage.removeItem(TOKEN_KEY); 9 | }; 10 | -------------------------------------------------------------------------------- /src/styles/global.js: -------------------------------------------------------------------------------- 1 | import { injectGlobal } from "styled-components"; 2 | 3 | import "font-awesome/css/font-awesome.css"; 4 | 5 | injectGlobal` 6 | * { 7 | box-sizing: border-box; 8 | padding: 0; 9 | margin: 0; 10 | outline: 0; 11 | } 12 | body, html { 13 | background: #eee; 14 | font-family: 'Helvetica Neue', 'Helvetica', Arial, sans-serif; 15 | text-rendering: optimizeLegibility !important; 16 | -webkit-font-smoothing: antialiased !important; 17 | height: 100%; 18 | width: 100%; 19 | } 20 | `; 21 | --------------------------------------------------------------------------------