├── .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 |
--------------------------------------------------------------------------------