├── .expo-shared └── assets.json ├── .gitignore ├── App.js ├── README.md ├── api.js ├── app.json ├── assets ├── icon.png ├── loginBg.jpeg ├── roomDefault.jpeg └── splash.png ├── babel.config.js ├── colors.js ├── components ├── Auth │ ├── BackBtn.js │ ├── Btn.js │ └── Input.js ├── DismissKeyboard.js ├── Gate.js ├── RoomCard.js └── RoomPhotos.js ├── navigation ├── Auth.js └── Main.js ├── package.json ├── redux ├── roomsSlice.js ├── rootReducer.js ├── store.js └── usersSlice.js ├── screens ├── Auth │ ├── SignIn │ │ ├── SignInContainer.js │ │ ├── SignInPresenter.js │ │ └── index.js │ ├── SignUp │ │ ├── SignUpContainer.js │ │ ├── SignUpPresenter.js │ │ └── index.js │ └── Welcome.js └── Main │ ├── Explore │ ├── ExploreContainer.js │ ├── ExplorePresenter.js │ └── index.js │ ├── Map │ ├── MapContainer.js │ ├── MapPresenter.js │ └── index.js │ ├── Profile.js │ ├── Room.js │ ├── Saved │ ├── SavedContainer.js │ ├── SavedPresenter.js │ └── index.js │ └── Search │ ├── SearchContainer.js │ ├── SearchPresenter.js │ └── index.js ├── utils.js └── yarn.lock /.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "f9155ac790fd02fadcdeca367b02581c04a353aa6d5aa84409a59f6804c87acd": true, 3 | "89ed26367cdb9b771858e026f2eb95bfdb90e5ae943e716575327ec325f39c44": true 4 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | .expo/* 3 | npm-debug.* 4 | *.jks 5 | *.p8 6 | *.p12 7 | *.key 8 | *.mobileprovision 9 | *.orig.* 10 | web-build/ 11 | web-report/ 12 | 13 | # macOS 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /App.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { AppLoading } from "expo"; 3 | import { Asset } from "expo-asset"; 4 | import * as Font from "expo-font"; 5 | import { Ionicons } from "@expo/vector-icons"; 6 | import { Image } from "react-native"; 7 | import { Provider } from "react-redux"; 8 | import { PersistGate } from "redux-persist/integration/react"; 9 | import Gate from "./components/Gate"; 10 | import store, { persistor } from "./redux/store"; 11 | 12 | const cacheImages = images => 13 | images.map(image => { 14 | if (typeof image === "string") { 15 | return Image.prefetch(image); 16 | } else { 17 | return Asset.fromModule(image).downloadAsync(); 18 | } 19 | }); 20 | 21 | const cacheFonts = fonts => fonts.map(font => Font.loadAsync(font)); 22 | 23 | export default function App() { 24 | const [isReady, setIsReady] = useState(false); 25 | const handleFinish = () => setIsReady(true); 26 | const loadAssets = async () => { 27 | const images = [ 28 | require("./assets/loginBg.jpeg"), 29 | require("./assets/roomDefault.jpeg"), 30 | "http://logok.org/wp-content/uploads/2014/07/airbnb-logo-belo-219x286.png" 31 | ]; 32 | const fonts = [Ionicons.font]; 33 | const imagePromises = cacheImages(images); 34 | const fontPromises = cacheFonts(fonts); 35 | return Promise.all([...fontPromises, ...imagePromises]); 36 | }; 37 | return isReady ? ( 38 | 39 | 40 | 41 | 42 | 43 | ) : ( 44 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Airbnb Clone App 2 | 3 | Beautiful Airbnb Clone built with React Native 4 | 5 | ### Screens: 6 | 7 | # Bluebnb Native 8 | 9 | ### Screens: 10 | 11 | - [ ] Login 12 | - [ ] Create Account 13 | - [ ] See Profile 14 | - [ ] Edit Profile 15 | - [ ] List Rooms 16 | - [ ] See Room 17 | - [ ] Add/Room From Favourites 18 | - [ ] Search Rooms 19 | -------------------------------------------------------------------------------- /api.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const callApi = async (method, path, data, jwt, params = {}) => { 4 | const headers = { 5 | Authorization: `Bearer ${jwt}`, 6 | "Content-Type": "application/json" 7 | }; 8 | const baseUrl = "http://127.0.0.1:8000/api/v1"; 9 | const fullUrl = `${baseUrl}${path}`; 10 | if (method === "get" || method === "delete") { 11 | return axios[method](fullUrl, { headers, params }); 12 | } else { 13 | return axios[method](fullUrl, data, { headers }); 14 | } 15 | }; 16 | 17 | export default { 18 | createAccount: form => callApi("post", "/users/", form), 19 | login: form => callApi("post", "/users/login/", form), 20 | rooms: (page = 1, token) => 21 | callApi("get", `/rooms/?page=${page}`, null, token), 22 | favs: (id, token) => callApi("get", `/users/${id}/favs/`, null, token), 23 | toggleFavs: (userId, roomId, token) => 24 | callApi("put", `/users/${userId}/favs/`, { pk: roomId }, token), 25 | search: (form, token) => callApi("get", "/rooms/search/", null, token, form) 26 | }; 27 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "Airbnb", 4 | "slug": "airbnb-native", 5 | "privacy": "public", 6 | "sdkVersion": "36.0.0", 7 | "platforms": ["ios", "android", "web"], 8 | "version": "1.0.0", 9 | "orientation": "portrait", 10 | "icon": "./assets/icon.png", 11 | "splash": { 12 | "image": "./assets/splash.png", 13 | "resizeMode": "contain", 14 | "backgroundColor": "#ffffff" 15 | }, 16 | "updates": { 17 | "fallbackToCacheTimeout": 0 18 | }, 19 | "assetBundlePatterns": ["**/*"], 20 | "ios": { 21 | "supportsTablet": true 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nomadcoders/airbnb-native/88243f33e1c60fe4808771c3cc51acf846e9e41b/assets/icon.png -------------------------------------------------------------------------------- /assets/loginBg.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nomadcoders/airbnb-native/88243f33e1c60fe4808771c3cc51acf846e9e41b/assets/loginBg.jpeg -------------------------------------------------------------------------------- /assets/roomDefault.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nomadcoders/airbnb-native/88243f33e1c60fe4808771c3cc51acf846e9e41b/assets/roomDefault.jpeg -------------------------------------------------------------------------------- /assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nomadcoders/airbnb-native/88243f33e1c60fe4808771c3cc51acf846e9e41b/assets/splash.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /colors.js: -------------------------------------------------------------------------------- 1 | export default { 2 | red: "#FF5A5F", 3 | black: "rgb(35, 35, 35)", 4 | green: "#006a70" 5 | }; 6 | -------------------------------------------------------------------------------- /components/Auth/BackBtn.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components/native"; 3 | import { Ionicons } from "@expo/vector-icons"; 4 | import utils from "../../utils"; 5 | 6 | const Container = styled.View` 7 | padding-left: 20px; 8 | `; 9 | 10 | export default () => ( 11 | 12 | 16 | 17 | ); 18 | -------------------------------------------------------------------------------- /components/Auth/Btn.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TouchableOpacity, Dimensions, ActivityIndicator } from "react-native"; 3 | import styled from "styled-components/native"; 4 | import PropTypes from "prop-types"; 5 | import colors from "../../colors"; 6 | 7 | const { width } = Dimensions.get("screen"); 8 | 9 | const Button = styled.View` 10 | margin-bottom: 25px; 11 | border: 1px solid ${props => (props.accent ? "transparent" : colors.black)}; 12 | border-radius: 30px; 13 | padding: 12.5px 0px; 14 | align-items: center; 15 | width: ${width / 1.5}px; 16 | background-color: ${props => (props.accent ? colors.red : "transparent")}; 17 | `; 18 | 19 | const Text = styled.Text` 20 | font-weight: 600; 21 | font-size: 14px; 22 | color: ${props => (props.accent ? "white" : colors.black)}; 23 | `; 24 | 25 | const Btn = ({ loading = false, onPress, text, accent = false }) => ( 26 | 27 | 34 | 35 | ); 36 | 37 | Btn.propTypes = { 38 | onPress: PropTypes.func.isRequired, 39 | text: PropTypes.string.isRequired, 40 | accent: PropTypes.bool, 41 | loading: PropTypes.bool 42 | }; 43 | 44 | export default Btn; 45 | -------------------------------------------------------------------------------- /components/Auth/Input.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { TouchableOpacity, Dimensions } from "react-native"; 3 | import styled from "styled-components/native"; 4 | import PropTypes from "prop-types"; 5 | 6 | const { width } = Dimensions.get("screen"); 7 | 8 | const Container = styled.TextInput` 9 | width: ${width / 1.5}px; 10 | padding: 12.5px 20px; 11 | border: 1px solid grey; 12 | background-color: white; 13 | border-radius: 30px; 14 | margin-bottom: 10px; 15 | font-weight: 500; 16 | `; 17 | 18 | const Input = ({ 19 | value, 20 | placeholder, 21 | isPassword = false, 22 | autoCapitalize, 23 | stateFn, 24 | keyboardType 25 | }) => ( 26 | stateFn(text)} 33 | /> 34 | ); 35 | 36 | Input.propTypes = { 37 | value: PropTypes.string, 38 | placeholder: PropTypes.string, 39 | isPassword: PropTypes.bool, 40 | autoCapitalize: PropTypes.string, 41 | stateFn: PropTypes.func.isRequired 42 | }; 43 | 44 | export default Input; 45 | -------------------------------------------------------------------------------- /components/DismissKeyboard.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Keyboard, TouchableWithoutFeedback } from "react-native"; 3 | 4 | export default ({ children }) => { 5 | const onPress = () => Keyboard.dismiss(); 6 | return ( 7 | 8 | {children} 9 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /components/Gate.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { View, Text, TouchableOpacity } from "react-native"; 3 | import { useSelector, useDispatch } from "react-redux"; 4 | import { logIn, logOut } from "../redux/usersSlice"; 5 | import Auth from "../navigation/Auth"; 6 | import { NavigationContainer } from "@react-navigation/native"; 7 | import Main from "../navigation/Main"; 8 | 9 | export default () => { 10 | const { isLoggedIn } = useSelector(state => state.usersReducer); 11 | const dispatch = useDispatch(); 12 | return ( 13 | 14 | {isLoggedIn ?
: } 15 | 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /components/RoomCard.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Pt from "prop-types"; 3 | import styled from "styled-components/native"; 4 | import { TouchableOpacity } from "react-native"; 5 | import { Ionicons } from "@expo/vector-icons"; 6 | import utils from "../utils"; 7 | import { useDispatch } from "react-redux"; 8 | import { toggleFav } from "../redux/usersSlice"; 9 | import colors from "../colors"; 10 | import { useNavigation } from "@react-navigation/native"; 11 | import RoomPhotos from "./RoomPhotos"; 12 | 13 | const Container = styled.View` 14 | width: 100%; 15 | margin-bottom: 25px; 16 | align-items: flex-start; 17 | position: relative; 18 | `; 19 | 20 | const Name = styled.Text` 21 | font-size: 18px; 22 | font-weight: 300; 23 | margin-bottom: 7px; 24 | `; 25 | 26 | const Superhost = styled.View` 27 | padding: 3px 5px; 28 | border: 1px solid black; 29 | border-radius: 4px; 30 | margin-bottom: 5px; 31 | `; 32 | 33 | const SuperhostText = styled.Text` 34 | text-transform: uppercase; 35 | font-weight: 500; 36 | font-size: 10px; 37 | `; 38 | 39 | const PriceContainer = styled.View` 40 | flex-direction: row; 41 | `; 42 | 43 | const PriceText = styled.Text` 44 | font-size: 16px; 45 | `; 46 | 47 | const PriceNumber = styled.Text` 48 | font-weight: 600; 49 | font-size: 16px; 50 | `; 51 | 52 | const FavButton = styled.View` 53 | background-color: white; 54 | width: 50px; 55 | height: 50px; 56 | border-radius: 25px; 57 | justify-content: center; 58 | align-items: center; 59 | `; 60 | 61 | const TOpacity = styled.TouchableOpacity` 62 | position: absolute; 63 | z-index: 10; 64 | right: 10px; 65 | top: 10px; 66 | `; 67 | 68 | function getIconName(isFav) { 69 | const isAndroid = utils.isAndroid(); 70 | if (isAndroid) { 71 | if (isFav) { 72 | return "md-heart"; 73 | } 74 | return "md-heart-empty"; 75 | } else { 76 | if (isFav) { 77 | return "ios-heart"; 78 | } 79 | return "ios-heart-empty"; 80 | } 81 | } 82 | 83 | const RoomCard = ({ id, isFav, isSuperHost, photos, name, price, roomObj }) => { 84 | const dispatch = useDispatch(); 85 | const navigation = useNavigation(); 86 | return ( 87 | 88 | dispatch(toggleFav(id))}> 89 | 90 | 95 | 96 | 97 | 98 | navigation.navigate("RoomDetail", { ...roomObj })} 101 | > 102 | {isSuperHost ? ( 103 | 104 | Superhost 105 | 106 | ) : null} 107 | {name} 108 | 109 | ${price} 110 | / night 111 | 112 | 113 | 114 | ); 115 | }; 116 | 117 | RoomCard.propTypes = { 118 | id: Pt.number.isRequired, 119 | isFav: Pt.bool.isRequired, 120 | isSuperHost: Pt.bool.isRequired, 121 | photos: Pt.arrayOf( 122 | Pt.shape({ 123 | file: Pt.string 124 | }) 125 | ), 126 | name: Pt.string.isRequired, 127 | price: Pt.number.isRequired, 128 | roomObj: Pt.object.isRequired 129 | }; 130 | 131 | export default RoomCard; 132 | -------------------------------------------------------------------------------- /components/RoomPhotos.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components/native"; 3 | import Pt from "prop-types"; 4 | import Swiper from "react-native-web-swiper"; 5 | import { Dimensions } from "react-native"; 6 | 7 | const { width, height } = Dimensions.get("screen"); 8 | 9 | const PhotosContainer = styled.View` 10 | margin-bottom: 10px; 11 | overflow: hidden; 12 | width: 100%; 13 | height: ${props => `${height / props.factor}`}px; 14 | border-radius: 4px; 15 | `; 16 | 17 | const SlideImage = styled.Image` 18 | width: 100%; 19 | height: 100%; 20 | `; 21 | 22 | const RoomPhotos = ({ photos, factor = 4 }) => ( 23 | 24 | {photos.length === 0 ? ( 25 | 29 | ) : ( 30 | null, 33 | NextComponent: () => null, 34 | dotActiveStyle: { 35 | backgroundColor: "white" 36 | } 37 | }} 38 | > 39 | {photos.map(photo => ( 40 | 41 | ))} 42 | 43 | )} 44 | 45 | ); 46 | 47 | RoomPhotos.propTypes = { 48 | photos: Pt.arrayOf( 49 | Pt.shape({ 50 | file: Pt.string 51 | }) 52 | ) 53 | }; 54 | 55 | export default RoomPhotos; 56 | -------------------------------------------------------------------------------- /navigation/Auth.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createStackNavigator } from "@react-navigation/stack"; 3 | import Welcome from "../screens/Auth/Welcome"; 4 | import SignIn from "../screens/Auth/SignIn"; 5 | import SignUp from "../screens/Auth/SignUp"; 6 | import BackBtn from "../components/Auth/BackBtn"; 7 | 8 | const Auth = createStackNavigator(); 9 | 10 | export default () => ( 11 | 17 | }} 18 | > 19 | 28 | 33 | 38 | 39 | ); 40 | -------------------------------------------------------------------------------- /navigation/Main.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createStackNavigator } from "@react-navigation/stack"; 3 | import { createBottomTabNavigator } from "@react-navigation/bottom-tabs"; 4 | import { Ionicons } from "@expo/vector-icons"; 5 | import Explore from "../screens/Main/Explore"; 6 | import Saved from "../screens/Main/Saved"; 7 | import MapScreen from "../screens/Main/Map"; 8 | import Profile from "../screens/Main/Profile"; 9 | import colors from "../colors"; 10 | import utils from "../utils"; 11 | import Room from "../screens/Main/Room"; 12 | import Search from "../screens/Main/Search"; 13 | import BackBtn from "../components/Auth/BackBtn"; 14 | import { BlurView } from "expo-blur"; 15 | import { StyleSheet } from "react-native"; 16 | 17 | const TabsNavigator = createBottomTabNavigator(); 18 | const Tabs = () => ( 19 | ({ 31 | tabBarIcon: ({ focused }) => { 32 | const isAndroid = utils.isAndroid(); 33 | let iconName = `${isAndroid ? "md-" : "ios-"}`; 34 | if (route.name === "Explore") { 35 | iconName += "search"; 36 | } else if (route.name === "Saved") { 37 | iconName += "heart"; 38 | } else if (route.name === "Map") { 39 | iconName += "map"; 40 | } else if (route.name === "Profile") { 41 | iconName += "person"; 42 | } 43 | return ( 44 | 49 | ); 50 | } 51 | })} 52 | > 53 | 54 | 55 | 56 | 57 | 58 | ); 59 | 60 | const MainNavigator = createStackNavigator(); 61 | export default () => ( 62 | 67 | }} 68 | > 69 | 74 | ( 80 | 85 | ) 86 | }} 87 | /> 88 | 93 | 94 | ); 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_modules/expo/AppEntry.js", 3 | "scripts": { 4 | "start": "expo start", 5 | "android": "expo start --android", 6 | "ios": "expo start --ios", 7 | "web": "expo start --web", 8 | "eject": "expo eject" 9 | }, 10 | "dependencies": { 11 | "@expo/vector-icons": "^10.0.6", 12 | "@react-native-community/masked-view": "0.1.5", 13 | "@react-navigation/bottom-tabs": "^5.2.4", 14 | "@react-navigation/native": "^5.1.3", 15 | "@react-navigation/stack": "^5.2.6", 16 | "@reduxjs/toolkit": "^1.3.0", 17 | "add": "^2.0.6", 18 | "axios": "^0.19.2", 19 | "expo": "~36.0.0", 20 | "expo-asset": "^8.0.0", 21 | "expo-blur": "~8.0.0", 22 | "expo-font": "~8.0.0", 23 | "prop-types": "^15.7.2", 24 | "react": "~16.9.0", 25 | "react-dom": "~16.9.0", 26 | "react-native": "https://github.com/expo/react-native/archive/sdk-36.0.0.tar.gz", 27 | "react-native-gesture-handler": "~1.5.0", 28 | "react-native-maps": "0.26.1", 29 | "react-native-reanimated": "~1.4.0", 30 | "react-native-safe-area-context": "0.6.0", 31 | "react-native-screens": "2.0.0-alpha.12", 32 | "react-native-web": "~0.11.7", 33 | "react-native-web-swiper": "^2.1.0", 34 | "react-redux": "^7.2.0", 35 | "redux": "^4.0.5", 36 | "redux-persist": "^6.0.0", 37 | "styled-components": "^5.0.1", 38 | "yarn": "^1.22.4" 39 | }, 40 | "devDependencies": { 41 | "@babel/core": "^7.0.0", 42 | "babel-preset-expo": "~8.0.0" 43 | }, 44 | "private": true 45 | } 46 | -------------------------------------------------------------------------------- /redux/roomsSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | import api from "../api"; 3 | 4 | const roomsSlice = createSlice({ 5 | name: "rooms", 6 | initialState: { 7 | explore: { 8 | page: 1, 9 | rooms: [] 10 | }, 11 | favs: [] 12 | }, 13 | reducers: { 14 | setExploreRooms(state, action) { 15 | const { payload } = action; 16 | if (payload.page === 1) { 17 | state.explore.rooms = payload.rooms; 18 | state.explore.page = 1; 19 | } else { 20 | state.explore.rooms = [...state.explore.rooms, ...payload.rooms]; 21 | } 22 | }, 23 | increasePage(state, action) { 24 | state.explore.page += 1; 25 | }, 26 | setFavs(state, action) { 27 | state.favs = action.payload; 28 | }, 29 | setFav(state, action) { 30 | const { 31 | payload: { roomId } 32 | } = action; 33 | const room = state.explore.rooms.find(room => room.id === roomId); 34 | if (room) { 35 | if (room.is_fav) { 36 | room.is_fav = false; 37 | state.favs = state.favs.filter(room => room.id !== roomId); 38 | } else { 39 | room.is_fav = true; 40 | state.favs.push(room); 41 | } 42 | } 43 | } 44 | } 45 | }); 46 | 47 | export const { 48 | setExploreRooms, 49 | increasePage, 50 | setFavs, 51 | setFav 52 | } = roomsSlice.actions; 53 | 54 | export const getRooms = page => async (dispatch, getState) => { 55 | const { 56 | usersReducer: { token } 57 | } = getState(); 58 | try { 59 | const { 60 | data: { results } 61 | } = await api.rooms(page, token); 62 | dispatch( 63 | setExploreRooms({ 64 | rooms: results, 65 | page 66 | }) 67 | ); 68 | } catch (e) { 69 | console.warn(e); 70 | } 71 | }; 72 | 73 | export default roomsSlice.reducer; 74 | -------------------------------------------------------------------------------- /redux/rootReducer.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from "redux"; 2 | import usersReducer from "./usersSlice"; 3 | import roomsReducer from "./roomsSlice"; 4 | 5 | export default combineReducers({ 6 | usersReducer, 7 | roomsReducer 8 | }); 9 | -------------------------------------------------------------------------------- /redux/store.js: -------------------------------------------------------------------------------- 1 | import { configureStore, getDefaultMiddleware } from "@reduxjs/toolkit"; 2 | import { 3 | persistStore, 4 | persistReducer, 5 | FLUSH, 6 | REHYDRATE, 7 | PAUSE, 8 | PERSIST, 9 | PURGE, 10 | REGISTER 11 | } from "redux-persist"; 12 | import rootReducer from "./rootReducer"; 13 | import { AsyncStorage } from "react-native"; 14 | 15 | const persistConfig = { 16 | key: "root", 17 | storage: AsyncStorage 18 | }; 19 | 20 | const persistedReducer = persistReducer(persistConfig, rootReducer); 21 | 22 | const store = configureStore({ 23 | reducer: persistedReducer, 24 | middleware: getDefaultMiddleware({ 25 | serializableCheck: { 26 | ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER] 27 | } 28 | }) 29 | }); 30 | 31 | export const persistor = persistStore(store); 32 | export default store; 33 | -------------------------------------------------------------------------------- /redux/usersSlice.js: -------------------------------------------------------------------------------- 1 | import { createSlice } from "@reduxjs/toolkit"; 2 | import api from "../api"; 3 | import { setFavs, setFav } from "./roomsSlice"; 4 | 5 | const userSlice = createSlice({ 6 | name: "users", 7 | initialState: { 8 | isLoggedIn: false, 9 | token: null 10 | }, 11 | reducers: { 12 | logIn(state, action) { 13 | state.isLoggedIn = true; 14 | state.token = action.payload.token; 15 | state.id = action.payload.id; 16 | }, 17 | logOut(state, action) { 18 | state.isLoggedIn = false; 19 | state.token = null; 20 | } 21 | } 22 | }); 23 | 24 | export const { logIn, logOut } = userSlice.actions; 25 | 26 | export const userLogin = form => async dispatch => { 27 | try { 28 | const { 29 | data: { id, token } 30 | } = await api.login(form); 31 | if (id && token) { 32 | dispatch(logIn({ token, id })); 33 | } 34 | } catch (e) { 35 | alert("Wrong user/password"); 36 | } 37 | }; 38 | 39 | export const getFavs = () => async (dispatch, getState) => { 40 | const { 41 | usersReducer: { id, token } 42 | } = getState(); 43 | try { 44 | const { data } = await api.favs(id, token); 45 | dispatch(setFavs(data)); 46 | } catch (e) { 47 | console.warn(e); 48 | } 49 | }; 50 | 51 | export const toggleFav = roomId => async (dispatch, getState) => { 52 | const { 53 | usersReducer: { id, token } 54 | } = getState(); 55 | dispatch(setFav({ roomId })); 56 | try { 57 | await api.toggleFavs(id, roomId, token); 58 | } catch (e) { 59 | console.warn(e); 60 | } 61 | }; 62 | 63 | export default userSlice.reducer; 64 | -------------------------------------------------------------------------------- /screens/Auth/SignIn/SignInContainer.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import utils from "../../../utils"; 3 | import { useDispatch } from "react-redux"; 4 | import { userLogin } from "../../../redux/usersSlice"; 5 | import SignInPresenter from "./SignInPresenter"; 6 | 7 | export default ({ route: { params } }) => { 8 | const dispatch = useDispatch(); 9 | const [email, setEmail] = useState(params?.email || "itnicolasme@gmail.com"); 10 | const [password, setPassword] = useState(params?.password || "12"); 11 | const isFormValid = () => { 12 | if (email === "" || password === "") { 13 | alert("All fields are required."); 14 | return false; 15 | } 16 | if (!utils.isEmail(email)) { 17 | alert("Email is invalid"); 18 | return false; 19 | } 20 | return true; 21 | }; 22 | const handleSubmit = () => { 23 | if (!isFormValid()) { 24 | return; 25 | } 26 | dispatch( 27 | userLogin({ 28 | username: email, 29 | password 30 | }) 31 | ); 32 | }; 33 | return ( 34 | 41 | ); 42 | }; 43 | -------------------------------------------------------------------------------- /screens/Auth/SignIn/SignInPresenter.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { StatusBar, KeyboardAvoidingView } from "react-native"; 3 | import styled from "styled-components/native"; 4 | import Btn from "../../../components/Auth/Btn"; 5 | import Input from "../../../components/Auth/Input"; 6 | import DismissKeyboard from "../../../components/DismissKeyboard"; 7 | 8 | const Container = styled.View` 9 | flex: 1; 10 | justify-content: center; 11 | align-items: center; 12 | `; 13 | 14 | const InputContainer = styled.View` 15 | margin-bottom: 30px; 16 | `; 17 | 18 | export default ({ email, setEmail, password, setPassword, handleSubmit }) => ( 19 | 20 | 21 | 22 | 23 | 24 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | ); 43 | -------------------------------------------------------------------------------- /screens/Auth/SignIn/index.js: -------------------------------------------------------------------------------- 1 | import SignInContainer from "./SignInContainer"; 2 | 3 | export default SignInContainer; 4 | -------------------------------------------------------------------------------- /screens/Auth/SignUp/SignUpContainer.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import utils from "../../utils"; 3 | import api from "../../api"; 4 | import SignUpPresenter from "./SignUpPresenter"; 5 | 6 | export default ({ navigation: { navigate } }) => { 7 | const [firstName, setFirstName] = useState(""); 8 | const [lastName, setLastName] = useState(""); 9 | const [email, setEmail] = useState(""); 10 | const [password, setPassword] = useState(""); 11 | const [loading, setLoading] = useState(false); 12 | const isFormValid = () => { 13 | if ( 14 | firstName === "" || 15 | lastName === "" || 16 | email === "" || 17 | password === "" 18 | ) { 19 | alert("All fields are required."); 20 | return false; 21 | } 22 | if (!utils.isEmail(email)) { 23 | alert("Please add a valid email."); 24 | return false; 25 | } 26 | return true; 27 | }; 28 | const handleSubmit = async () => { 29 | if (!isFormValid()) { 30 | return; 31 | } 32 | setLoading(true); 33 | try { 34 | const { status } = await api.createAccount({ 35 | first_name: firstName, 36 | last_name: lastName, 37 | email, 38 | username: email, 39 | password 40 | }); 41 | if (status === 201) { 42 | alert("Account created. Sign in, please."); 43 | navigate("SignIn", { email, password }); 44 | } 45 | } catch (e) { 46 | alert("The email is taken"); 47 | } finally { 48 | setLoading(false); 49 | } 50 | }; 51 | return ( 52 | 64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /screens/Auth/SignUp/SignUpPresenter.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { StatusBar, KeyboardAvoidingView } from "react-native"; 3 | import styled from "styled-components/native"; 4 | import Btn from "../../components/Auth/Btn"; 5 | import Input from "../../components/Auth/Input"; 6 | import DismissKeyboard from "../../components/DismissKeyboard"; 7 | 8 | const Container = styled.View` 9 | flex: 1; 10 | justify-content: center; 11 | align-items: center; 12 | `; 13 | 14 | const InputContainer = styled.View` 15 | margin-bottom: 30px; 16 | `; 17 | 18 | export default ({ 19 | firstName, 20 | setFirstName, 21 | lastName, 22 | setLastName, 23 | email, 24 | setEmail, 25 | password, 26 | setPassword, 27 | loading, 28 | handleSubmit 29 | }) => ( 30 | 31 | 32 | 33 | 34 | 35 | 41 | 47 | 55 | 61 | 62 | 68 | 69 | 70 | 71 | ); 72 | -------------------------------------------------------------------------------- /screens/Auth/SignUp/index.js: -------------------------------------------------------------------------------- 1 | import SignInContainer from "../SignIn"; 2 | 3 | export default SignInContainer; 4 | -------------------------------------------------------------------------------- /screens/Auth/Welcome.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components/native"; 3 | import { StatusBar } from "react-native"; 4 | import { BlurView } from "expo-blur"; 5 | import Btn from "../../components/Auth/Btn"; 6 | 7 | const LOGO_URL = 8 | "http://logok.org/wp-content/uploads/2014/07/airbnb-logo-belo-219x286.png"; 9 | 10 | const Container = styled.View` 11 | flex: 1; 12 | `; 13 | 14 | const Image = styled.Image` 15 | position: absolute; 16 | z-index: -1; 17 | top: 0; 18 | `; 19 | 20 | const Logo = styled.Image` 21 | margin-top: 50px; 22 | width: 100px; 23 | height: 100px; 24 | `; 25 | 26 | const BtnContainer = styled.View` 27 | margin-top: 40px; 28 | `; 29 | 30 | export default ({ navigation }) => { 31 | const goToSignUp = () => navigation.navigate("SignUp"); 32 | const goToSignIn = () => navigation.navigate("SignIn"); 33 | return ( 34 | 35 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /screens/Main/Explore/ExploreContainer.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from "react"; 2 | import ExplorePresenter from "./ExplorePresenter"; 3 | 4 | export default ({ getRooms, rooms, increasePage, page }) => { 5 | useEffect(() => { 6 | getRooms(1); 7 | }, []); 8 | useEffect(() => { 9 | getRooms(page); 10 | }, [page]); 11 | return ; 12 | }; 13 | -------------------------------------------------------------------------------- /screens/Main/Explore/ExplorePresenter.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components/native"; 3 | import { 4 | ActivityIndicator, 5 | ScrollView, 6 | TouchableOpacity, 7 | TouchableWithoutFeedback 8 | } from "react-native"; 9 | import RoomCard from "../../../components/RoomCard"; 10 | import { useNavigation } from "@react-navigation/native"; 11 | 12 | const Container = styled.View` 13 | flex: 1; 14 | justify-content: center; 15 | align-items: center; 16 | padding-horizontal: 15px; 17 | `; 18 | 19 | const FakeBar = styled.View` 20 | height: 40px; 21 | width: 100%; 22 | background-color: white; 23 | box-shadow: 1px 5px 5px rgba(200, 200, 200, 0.5); 24 | margin: 80px 0px 10px 0px; 25 | border-radius: 7px; 26 | justify-content: center; 27 | padding-left: 10px; 28 | `; 29 | 30 | const FakeText = styled.Text` 31 | font-size: 14px; 32 | font-weight: 300; 33 | `; 34 | 35 | const LoadMore = styled.View` 36 | width: 100%; 37 | padding: 10px 10px; 38 | align-items: center; 39 | background-color: #006a70; 40 | border-radius: 5px; 41 | margin-bottom: 30px; 42 | `; 43 | 44 | const LoadMoreText = styled.Text` 45 | color: white; 46 | font-size: 18px; 47 | font-weight: 500; 48 | `; 49 | 50 | export default ({ rooms, increasePage }) => { 51 | const navigation = useNavigation(); 52 | return ( 53 | 54 | {rooms.length === 0 ? ( 55 | 56 | ) : ( 57 | <> 58 | navigation.navigate("Search")} 60 | > 61 | 62 | Search... 63 | 64 | 65 | 70 | {rooms.map(room => ( 71 | 81 | ))} 82 | 83 | 84 | Load More 85 | 86 | 87 | 88 | 89 | )} 90 | 91 | ); 92 | }; 93 | -------------------------------------------------------------------------------- /screens/Main/Explore/index.js: -------------------------------------------------------------------------------- 1 | import ExploreContainer from "./ExploreContainer"; 2 | import { connect } from "react-redux"; 3 | import { getRooms, increasePage } from "../../../redux/roomsSlice"; 4 | 5 | function mapDispatchToProps(dispatch) { 6 | return { 7 | getRooms: page => dispatch(getRooms(page)), 8 | increasePage: () => dispatch(increasePage()) 9 | }; 10 | } 11 | 12 | function mapStateToProps(state) { 13 | return state.roomsReducer.explore; 14 | } 15 | 16 | export default connect(mapStateToProps, mapDispatchToProps)(ExploreContainer); 17 | -------------------------------------------------------------------------------- /screens/Main/Map/MapContainer.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef } from "react"; 2 | import { Dimensions } from "react-native"; 3 | import MapPresenter from "./MapPresenter"; 4 | 5 | const { width, height } = Dimensions.get("screen"); 6 | 7 | export default ({ rooms }) => { 8 | const mapRef = useRef(); 9 | const [currentIndex, setCurrentIndex] = useState(0); 10 | const onScroll = e => { 11 | const { 12 | nativeEvent: { 13 | contentOffset: { x } 14 | } 15 | } = e; 16 | const position = Math.abs(Math.round(x / width)); 17 | setCurrentIndex(position); 18 | }; 19 | const moveMap = () => { 20 | mapRef.current?.animateCamera( 21 | { 22 | center: { 23 | latitude: parseFloat(rooms[currentIndex].lat), 24 | longitude: parseFloat(rooms[currentIndex].lng) 25 | } 26 | }, 27 | { duration: 3000 } 28 | ); 29 | }; 30 | useEffect(() => { 31 | if (currentIndex !== 0) { 32 | moveMap(); 33 | } 34 | }, [currentIndex]); 35 | const onRegionChangeComplete = async () => { 36 | try { 37 | const { northEast, southWest } = await mapRef.current?.getMapBoundaries(); 38 | console.log(northEast, southWest); 39 | } catch (e) { 40 | console.warn(e); 41 | } 42 | }; 43 | return ( 44 | 51 | ); 52 | }; 53 | -------------------------------------------------------------------------------- /screens/Main/Map/MapPresenter.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components/native"; 3 | import { StyleSheet, Dimensions } from "react-native"; 4 | import MapView, { Marker } from "react-native-maps"; 5 | import colors from "../../../colors"; 6 | 7 | const { width, height } = Dimensions.get("screen"); 8 | 9 | const Container = styled.View` 10 | flex: 1; 11 | justify-content: center; 12 | align-items: center; 13 | `; 14 | 15 | const ScrollView = styled.ScrollView` 16 | position: absolute; 17 | bottom: 50px; 18 | `; 19 | 20 | const RoomContainer = styled.View` 21 | background-color: transparent; 22 | width: ${width}px; 23 | align-items: center; 24 | `; 25 | 26 | const RoomCard = styled.View` 27 | background-color: white; 28 | width: ${width - 80}px; 29 | height: 120px; 30 | margin-right: 20px; 31 | border-radius: 10px; 32 | padding: 0px 20px; 33 | flex-direction: row; 34 | align-items: center; 35 | `; 36 | 37 | const RoomPhoto = styled.Image` 38 | width: 80px; 39 | height: 80px; 40 | border-radius: 5px; 41 | margin-right: 20px; 42 | `; 43 | 44 | const Column = styled.View` 45 | width: 70%; 46 | `; 47 | 48 | const RoomName = styled.Text` 49 | font-size: 18px; 50 | `; 51 | 52 | const RoomPrice = styled.Text` 53 | margin-top: 5px; 54 | font-size: 16px; 55 | `; 56 | 57 | const MarkerWrapper = styled.View` 58 | align-items: center; 59 | `; 60 | 61 | const MarkerContainer = styled.View` 62 | background-color: ${props => (props.selected ? colors.red : colors.green)}; 63 | padding: 10px; 64 | border-radius: 10px; 65 | position: relative; 66 | `; 67 | const MarkerText = styled.Text` 68 | color: white; 69 | font-size: 18px; 70 | font-weight: 600; 71 | `; 72 | const MarkerTriangle = styled.View` 73 | border: 10px solid transparent; 74 | width: 10px; 75 | border-top-color: ${props => (props.selected ? colors.red : colors.green)}; 76 | `; 77 | 78 | const RoomMarker = ({ selected, price }) => ( 79 | 80 | 81 | ${price} 82 | 83 | 84 | 85 | ); 86 | 87 | export default ({ 88 | rooms, 89 | mapRef, 90 | currentIndex, 91 | onScroll, 92 | onRegionChangeComplete 93 | }) => ( 94 | 95 | 110 | {rooms?.map((room, index) => ( 111 | 118 | 119 | 120 | ))} 121 | 122 | 129 | {rooms?.map(room => ( 130 | 131 | 132 | 139 | 140 | {room.name} 141 | ${room.price} 142 | 143 | 144 | 145 | ))} 146 | 147 | 148 | ); 149 | -------------------------------------------------------------------------------- /screens/Main/Map/index.js: -------------------------------------------------------------------------------- 1 | import MapContainer from "./MapContainer"; 2 | import { connect } from "react-redux"; 3 | 4 | function mapStateToProps(state) { 5 | return { rooms: state.roomsReducer.explore.rooms }; 6 | } 7 | 8 | export default connect(mapStateToProps)(MapContainer); 9 | -------------------------------------------------------------------------------- /screens/Main/Profile.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components/native"; 3 | 4 | const Container = styled.View` 5 | flex: 1; 6 | justify-content: center; 7 | align-items: center; 8 | `; 9 | 10 | const Text = styled.Text``; 11 | 12 | export default () => ( 13 | 14 | Profile 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /screens/Main/Room.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import styled from "styled-components/native"; 3 | import { Ionicons } from "@expo/vector-icons"; 4 | import MapView, { Marker } from "react-native-maps"; 5 | import RoomPhotos from "../../components/RoomPhotos"; 6 | import colors from "../../colors"; 7 | import utils from "../../utils"; 8 | 9 | const Container = styled.ScrollView``; 10 | 11 | const DataContainer = styled.View` 12 | padding: 0px 20px; 13 | `; 14 | 15 | const Address = styled.Text` 16 | margin-top: 10px; 17 | font-size: 24px; 18 | `; 19 | 20 | const PropertyInfoContainer = styled.View` 21 | margin-top: 20px; 22 | flex-direction: row; 23 | `; 24 | 25 | const PropertyInfoData = styled.View` 26 | background-color: ${colors.green}; 27 | margin-right: 10px; 28 | border-radius: 5px; 29 | `; 30 | 31 | const PropertyInfoText = styled.Text` 32 | color: white; 33 | font-weight: 500; 34 | padding: 5px 10px; 35 | `; 36 | 37 | const CheckContainer = styled.View` 38 | margin-top: 40px; 39 | `; 40 | 41 | const CheckTitleContainer = styled.View` 42 | flex-direction: row; 43 | align-items: center; 44 | `; 45 | 46 | const CheckTitle = styled.Text` 47 | font-size: 18px; 48 | margin-left: 15px; 49 | `; 50 | 51 | const CheckTime = styled.Text` 52 | margin-top: 10px; 53 | `; 54 | 55 | const MapContainer = styled.View` 56 | width: 100%; 57 | height: 200px; 58 | margin-top: 30px; 59 | `; 60 | 61 | function formatQtt(number, name) { 62 | if (number === 1) { 63 | return `${number} ${name}`; 64 | } else { 65 | return `${number} ${name}s`; 66 | } 67 | } 68 | 69 | function formatTime(time) { 70 | const [hours, min, sec] = time.split(":"); 71 | return `${hours} o'clock.`; 72 | } 73 | 74 | export default ({ route: { params }, navigation }) => { 75 | useEffect(() => { 76 | navigation.setOptions({ title: params.name }); 77 | }, []); 78 | return ( 79 | 80 | 81 | 82 |
83 | {params.address} / ${params.price} 84 |
85 | 86 | 87 | {formatQtt(params.beds, "bed")} 88 | 89 | 90 | 91 | {formatQtt(params.bedrooms, "bedroom")} 92 | 93 | 94 | 95 | 96 | {formatQtt(params.bathrooms, "bathroom")} 97 | 98 | 99 | 100 | 101 | 102 | 106 | Check-in / Check-out 107 | 108 | 109 | {formatTime(params.check_in)} / {formatTime(params.check_out)} 110 | 111 | 112 | 113 | 128 | 134 | 135 | 136 |
137 |
138 | ); 139 | }; 140 | -------------------------------------------------------------------------------- /screens/Main/Saved/SavedContainer.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import SavedPresenter from "./SavedPresenter"; 3 | 4 | export default ({ getFavs, rooms }) => { 5 | useEffect(() => { 6 | getFavs(); 7 | }, []); 8 | return ; 9 | }; 10 | -------------------------------------------------------------------------------- /screens/Main/Saved/SavedPresenter.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components/native"; 3 | import RoomCard from "../../../components/RoomCard"; 4 | 5 | const Container = styled.View` 6 | margin-top: 70px; 7 | padding: 0px 30px; 8 | `; 9 | 10 | const SV = styled.ScrollView``; 11 | 12 | const Title = styled.Text` 13 | font-size: 36px; 14 | margin-bottom: 10px; 15 | `; 16 | 17 | const NoFavs = styled.Text``; 18 | 19 | export default ({ rooms }) => ( 20 | 21 | Favourites ({rooms.length}) 22 | 26 | {rooms.length !== 0 ? ( 27 | rooms.map(room => ( 28 | 38 | )) 39 | ) : ( 40 | You don't have any favs. 41 | )} 42 | 43 | 44 | ); 45 | -------------------------------------------------------------------------------- /screens/Main/Saved/index.js: -------------------------------------------------------------------------------- 1 | import SavedContainer from "./SavedContainer"; 2 | import { getFavs } from "../../../redux/usersSlice"; 3 | import { connect } from "react-redux"; 4 | 5 | function mapStateToProps(state) { 6 | return { rooms: state.roomsReducer.favs }; 7 | } 8 | 9 | function mapDispatchToProps(dispatch) { 10 | return { 11 | getFavs: () => dispatch(getFavs()) 12 | }; 13 | } 14 | 15 | export default connect(mapStateToProps, mapDispatchToProps)(SavedContainer); 16 | -------------------------------------------------------------------------------- /screens/Main/Search/SearchContainer.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from "react"; 2 | import { useNavigation } from "@react-navigation/native"; 3 | import { Keyboard } from "react-native"; 4 | import SearchPresenter from "./SearchPresenter"; 5 | 6 | import api from "../../../api"; 7 | 8 | export default ({ token }) => { 9 | const navigation = useNavigation(); 10 | const [searching, setSearching] = useState(false); 11 | const [beds, setBeds] = useState(); 12 | const [bedrooms, setBedrooms] = useState(); 13 | const [bathrooms, setBathrooms] = useState(); 14 | const [maxPrice, setMaxPrice] = useState(); 15 | const [results, setResults] = useState(); 16 | const triggerSearch = async () => { 17 | setSearching(true); 18 | const form = { 19 | ...(beds && { beds }), 20 | ...(bedrooms && { bedrooms }), 21 | ...(bathrooms && { bathrooms }), 22 | ...(maxPrice && { max_price: maxPrice }) 23 | }; 24 | try { 25 | const { data } = await api.search(form, token); 26 | console.log(data); 27 | setResults(data); 28 | } catch (e) { 29 | console.warn(e); 30 | } finally { 31 | Keyboard.dismiss(); 32 | setSearching(false); 33 | } 34 | }; 35 | return ( 36 | 50 | ); 51 | }; 52 | -------------------------------------------------------------------------------- /screens/Main/Search/SearchPresenter.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components/native"; 3 | import DismissKeyboard from "../../../components/DismissKeyboard"; 4 | import { ActivityIndicator } from "react-native"; 5 | import colors from "../../../colors"; 6 | import RoomCard from "../../../components/RoomCard"; 7 | 8 | const Container = styled.View` 9 | padding: 0px; 10 | `; 11 | 12 | const SearchContainer = styled.View` 13 | margin-top: 50px; 14 | flex-direction: row; 15 | justify-content: space-between; 16 | align-items: center; 17 | padding: 10px 20px; 18 | `; 19 | 20 | const SearchBar = styled.TextInput` 21 | height: 40px; 22 | width: 80%; 23 | background-color: white; 24 | box-shadow: 1px 5px 5px rgba(200, 200, 200, 0.5); 25 | border-radius: 7px; 26 | justify-content: center; 27 | padding-left: 10px; 28 | `; 29 | 30 | const CancelContainer = styled.TouchableOpacity``; 31 | 32 | const CancelText = styled.Text``; 33 | 34 | const FiltersContainer = styled.ScrollView` 35 | flex-direction: row; 36 | margin-top: 10px; 37 | `; 38 | 39 | const FilterContainer = styled.View` 40 | align-items: center; 41 | margin-right: 15px; 42 | `; 43 | 44 | const FilterLabel = styled.Text` 45 | text-transform: uppercase; 46 | font-size: 12px; 47 | margin-bottom: 5px; 48 | font-weight: 500; 49 | `; 50 | 51 | const Filter = styled.TextInput` 52 | padding: 10px; 53 | background-color: white; 54 | border-radius: 20px; 55 | box-shadow: 1px 2.5px 2.5px rgba(200, 200, 200, 0.5); 56 | width: 80px; 57 | `; 58 | 59 | const SearchBtn = styled.TouchableOpacity` 60 | background-color: ${colors.red}; 61 | padding: 10px; 62 | margin: 10px 30px; 63 | border-radius: 10px; 64 | align-items: center; 65 | `; 66 | 67 | const SearchText = styled.Text` 68 | color: white; 69 | font-weight: 600; 70 | font-size: 16px; 71 | `; 72 | 73 | const ResultsText = styled.Text` 74 | margin-top: 10px; 75 | font-size: 16px; 76 | text-align: center; 77 | `; 78 | 79 | const Results = styled.ScrollView` 80 | margin-top: 25px; 81 | `; 82 | 83 | export default ({ 84 | navigation, 85 | beds, 86 | setBeds, 87 | bedrooms, 88 | setBedrooms, 89 | bathrooms, 90 | setBathrooms, 91 | maxPrice, 92 | setMaxPrice, 93 | searching, 94 | triggerSearch, 95 | results 96 | }) => ( 97 | 98 | <> 99 | 100 | 101 | 102 | navigation.goBack()}> 103 | Cancel 104 | 105 | 106 | 114 | 115 | Beds 116 | setBeds(text)} 118 | value={beds} 119 | placeholder="0" 120 | keyboardType={"number-pad"} 121 | /> 122 | 123 | 124 | Bedrooms 125 | setBedrooms(text)} 127 | value={bedrooms} 128 | placeholder="0" 129 | keyboardType={"number-pad"} 130 | /> 131 | 132 | 133 | Bathrooms 134 | setBathrooms(text)} 136 | value={bathrooms} 137 | placeholder="0" 138 | keyboardType={"number-pad"} 139 | /> 140 | 141 | 142 | Max. price 143 | setMaxPrice(text)} 145 | value={maxPrice} 146 | placeholder="$0" 147 | keyboardType={"number-pad"} 148 | /> 149 | 150 | 151 | 152 | 153 | {searching ? ( 154 | 155 | ) : ( 156 | Search 157 | )} 158 | 159 | {results ? ( 160 | Showing {results.count} results 161 | ) : null} 162 | 163 | {results?.results?.map(room => ( 164 | 174 | ))} 175 | 176 | 177 | 178 | ); 179 | -------------------------------------------------------------------------------- /screens/Main/Search/index.js: -------------------------------------------------------------------------------- 1 | import SearchContainer from "./SearchContainer"; 2 | import { connect } from "react-redux"; 3 | 4 | function mapStateToProps(state) { 5 | return { token: state.usersReducer.token }; 6 | } 7 | 8 | export default connect(mapStateToProps)(SearchContainer); 9 | -------------------------------------------------------------------------------- /utils.js: -------------------------------------------------------------------------------- 1 | import { Platform } from "react-native"; 2 | 3 | export default { 4 | isAndroid: () => Platform.OS === "android", 5 | isEmail: email => { 6 | const regEx = /(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/; 7 | return regEx.test(email); 8 | } 9 | }; 10 | --------------------------------------------------------------------------------