├── .gitignore
├── DUMMY.env
├── README.md
├── app.json
├── app
├── (modals)
│ ├── booking.tsx
│ └── login.tsx
├── (tabs)
│ ├── _layout.tsx
│ ├── inbox.tsx
│ ├── index.tsx
│ ├── profile.tsx
│ ├── trips.tsx
│ └── whishlists.tsx
├── _layout.tsx
└── listing
│ └── [id].tsx
├── assets
├── data
│ ├── airbnb-listings.geo.json
│ ├── airbnb-listings.json
│ ├── places.ts
│ ├── world-0.png
│ ├── world-1.png
│ ├── world-2.png
│ ├── world-3.png
│ ├── world-4.png
│ └── world-5.png
├── fonts
│ ├── Montserrat-Bold.ttf
│ ├── Montserrat-Regular.ttf
│ └── Montserrat-SemiBold.ttf
└── images
│ ├── adaptive-icon.png
│ ├── favicon.png
│ ├── google.svg
│ ├── icon.png
│ └── splash.png
├── babel.config.js
├── banner.png
├── components
├── ExploreHeader.tsx
├── Listings.tsx
├── ListingsBottomSheet.tsx
├── ListingsMap.tsx
└── ModalHeaderText.tsx
├── constants
├── Colors.ts
└── Styles.ts
├── expo-env.d.ts
├── hooks
└── useWarmUpBrowser.ts
├── metro.config.js
├── package-lock.json
├── package.json
├── screenshots
├── 1.png
├── 2.png
├── 3.png
├── 4.png
├── 5.png
└── demo.gif
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
2 |
3 | # dependencies
4 | node_modules/
5 |
6 | # Expo
7 | .expo/
8 | dist/
9 | web-build/
10 |
11 | # Native
12 | *.orig.*
13 | *.jks
14 | *.p8
15 | *.p12
16 | *.key
17 | *.mobileprovision
18 |
19 | # Metro
20 | .metro-health-check*
21 |
22 | # debug
23 | npm-debug.*
24 | yarn-debug.*
25 | yarn-error.*
26 |
27 | # macOS
28 | .DS_Store
29 | *.pem
30 |
31 | # local env files
32 | .env*.local
33 |
34 | # typescript
35 | *.tsbuildinfo
36 |
37 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb
38 | # The following patterns were generated by expo-cli
39 |
40 | expo-env.d.ts
41 | # @end expo-cli
42 |
43 | .env
--------------------------------------------------------------------------------
/DUMMY.env:
--------------------------------------------------------------------------------
1 | EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Native AirBnB Clone with Clerk
2 |
3 | This is a React Native AirBnB clone using [Clerk](https://clerk.com/?utm_source=sponsorship&utm_medium=github&utm_campaign=simong&utm_content=rn-airbnb) for user authentication.
4 |
5 | Additional features:
6 |
7 | - [Expo Router](https://docs.expo.dev/routing/introduction/) file-based navigation
8 | - [Google](https://clerk.com/docs/authentication/social-connections/google?utm_source=sponsorship&utm_medium=github&utm_campaign=simong&utm_content=rn-airbnb) & [Apple](https://clerk.com/docs/authentication/social-connections/apple?utm_source=sponsorship&utm_medium=github&utm_campaign=simong&utm_content=rn-airbnb) Auth with Clerk
9 | - [Reanimated](https://reanimated-beta-docs.swmansion.com/) 3 for animations
10 | - [MapView](https://docs.expo.dev/versions/latest/sdk/map-view/) with Marker and [Clustering](https://github.com/venits/react-native-map-clustering)
11 | - [Bottom Sheet](https://gorhom.github.io/react-native-bottom-sheet/)
12 | - Modal with Animations and Blurred Background
13 |
14 | ## Screenshots
15 |
16 |
17 |

18 |

19 |

20 |

21 |

22 |
23 |
24 |
25 | ## Demo
26 |
27 | 
28 |
29 | ## 🚀 More
30 |
31 | **Take a shortcut from web developer to mobile development fluency with guided learning**
32 |
33 | Enjoyed this project? Learn to use React Native to build production-ready, native mobile apps for both iOS and Android based on your existing web development skills.
34 |
35 |
36 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "airbnb",
4 | "slug": "airbnb",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "icon": "./assets/images/icon.png",
8 | "scheme": "myapp",
9 | "userInterfaceStyle": "automatic",
10 | "splash": {
11 | "image": "./assets/images/splash.png",
12 | "resizeMode": "contain",
13 | "backgroundColor": "#ffffff"
14 | },
15 | "assetBundlePatterns": ["**/*"],
16 | "ios": {
17 | "supportsTablet": true
18 | },
19 | "android": {
20 | "adaptiveIcon": {
21 | "foregroundImage": "./assets/images/adaptive-icon.png",
22 | "backgroundColor": "#ffffff"
23 | }
24 | },
25 | "web": {
26 | "bundler": "metro",
27 | "output": "static",
28 | "favicon": "./assets/images/favicon.png"
29 | },
30 | "plugins": [
31 | "expo-router",
32 | [
33 | "expo-location",
34 | {
35 | "locationAlwaysAndWhenInUsePermission": "Allow $(PRODUCT_NAME) to use your location."
36 | }
37 | ],
38 | [
39 | "expo-image-picker",
40 | {
41 | "photosPermission": "The app accesses your photos to let you share them with your friends."
42 | }
43 | ]
44 | ],
45 | "experiments": {
46 | "typedRoutes": true,
47 | "tsconfigPaths": true
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/app/(modals)/booking.tsx:
--------------------------------------------------------------------------------
1 | import { View, Text, StyleSheet, ScrollView, Image, SafeAreaView } from 'react-native';
2 | import { useState } from 'react';
3 | import Animated, { FadeIn, FadeOut, SlideInDown } from 'react-native-reanimated';
4 | import { Ionicons } from '@expo/vector-icons';
5 | import { BlurView } from 'expo-blur';
6 | import { TextInput } from 'react-native-gesture-handler';
7 | import { TouchableOpacity } from '@gorhom/bottom-sheet';
8 | import { defaultStyles } from '@/constants/Styles';
9 | import Colors from '@/constants/Colors';
10 | import { places } from '@/assets/data/places';
11 | import { useRouter } from 'expo-router';
12 | // @ts-ignore
13 | import DatePicker from 'react-native-modern-datepicker';
14 |
15 | const AnimatedTouchableOpacity = Animated.createAnimatedComponent(TouchableOpacity);
16 |
17 | const guestsGropus = [
18 | {
19 | name: 'Adults',
20 | text: 'Ages 13 or above',
21 | count: 0,
22 | },
23 | {
24 | name: 'Children',
25 | text: 'Ages 2-12',
26 | count: 0,
27 | },
28 | {
29 | name: 'Infants',
30 | text: 'Under 2',
31 | count: 0,
32 | },
33 | {
34 | name: 'Pets',
35 | text: 'Pets allowed',
36 | count: 0,
37 | },
38 | ];
39 |
40 | const Page = () => {
41 | const [openCard, setOpenCard] = useState(0);
42 | const [selectedPlace, setSelectedPlace] = useState(0);
43 |
44 | const [groups, setGroups] = useState(guestsGropus);
45 | const router = useRouter();
46 | const today = new Date().toISOString().substring(0, 10);
47 |
48 | const onClearAll = () => {
49 | setSelectedPlace(0);
50 | setOpenCard(0);
51 | };
52 |
53 | return (
54 |
55 | {/* Where */}
56 |
57 | {openCard != 0 && (
58 | setOpenCard(0)}
60 | style={styles.cardPreview}
61 | entering={FadeIn.duration(200)}
62 | exiting={FadeOut.duration(200)}>
63 | Where
64 | I'm flexible
65 |
66 | )}
67 |
68 | {openCard == 0 && Where to?}
69 | {openCard == 0 && (
70 |
71 |
72 |
73 |
78 |
79 |
80 |
84 | {places.map((item, index) => (
85 | setSelectedPlace(index)} key={index}>
86 |
90 | {item.title}
91 |
92 | ))}
93 |
94 |
95 | )}
96 |
97 |
98 | {/* When */}
99 |
100 | {openCard != 1 && (
101 | setOpenCard(1)}
103 | style={styles.cardPreview}
104 | entering={FadeIn.duration(200)}
105 | exiting={FadeOut.duration(200)}>
106 | When
107 | Any week
108 |
109 | )}
110 |
111 | {openCard == 1 && When's your trip?}
112 |
113 | {openCard == 1 && (
114 |
115 |
126 |
127 | )}
128 |
129 |
130 | {/* Guests */}
131 |
132 | {openCard != 2 && (
133 | setOpenCard(2)}
135 | style={styles.cardPreview}
136 | entering={FadeIn.duration(200)}
137 | exiting={FadeOut.duration(200)}>
138 | Who
139 | Add guests
140 |
141 | )}
142 |
143 | {openCard == 2 && Who's coming?}
144 |
145 | {openCard == 2 && (
146 |
147 | {groups.map((item, index) => (
148 |
154 |
155 | {item.name}
156 |
157 | {item.text}
158 |
159 |
160 |
161 |
168 | {
170 | const newGroups = [...groups];
171 | newGroups[index].count =
172 | newGroups[index].count > 0 ? newGroups[index].count - 1 : 0;
173 |
174 | setGroups(newGroups);
175 | }}>
176 | 0 ? Colors.grey : '#cdcdcd'}
180 | />
181 |
182 |
189 | {item.count}
190 |
191 | {
193 | const newGroups = [...groups];
194 | newGroups[index].count++;
195 | setGroups(newGroups);
196 | }}>
197 |
198 |
199 |
200 |
201 | ))}
202 |
203 | )}
204 |
205 |
206 | {/* Footer */}
207 |
208 |
210 |
213 |
219 | Clear all
220 |
221 |
222 |
223 | router.back()}>
226 |
232 | Search
233 |
234 |
235 |
236 |
237 | );
238 | };
239 |
240 | const styles = StyleSheet.create({
241 | container: {
242 | flex: 1,
243 | paddingTop: 100,
244 | },
245 | card: {
246 | backgroundColor: '#fff',
247 | borderRadius: 14,
248 | margin: 10,
249 | elevation: 4,
250 | shadowColor: '#000',
251 | shadowOpacity: 0.3,
252 | shadowRadius: 4,
253 | shadowOffset: {
254 | width: 2,
255 | height: 2,
256 | },
257 | gap: 20,
258 | },
259 | cardHeader: {
260 | fontFamily: 'mon-b',
261 | fontSize: 24,
262 | padding: 20,
263 | },
264 | cardBody: {
265 | paddingHorizontal: 20,
266 | paddingBottom: 20,
267 | },
268 | cardPreview: {
269 | flexDirection: 'row',
270 | justifyContent: 'space-between',
271 | padding: 20,
272 | },
273 |
274 | searchSection: {
275 | height: 50,
276 | flexDirection: 'row',
277 | justifyContent: 'center',
278 | alignItems: 'center',
279 | backgroundColor: '#fff',
280 | borderWidth: 1,
281 | borderColor: '#ABABAB',
282 | borderRadius: 8,
283 | marginBottom: 16,
284 | },
285 | searchIcon: {
286 | padding: 10,
287 | },
288 | inputField: {
289 | flex: 1,
290 | padding: 10,
291 | backgroundColor: '#fff',
292 | },
293 | placesContainer: {
294 | flexDirection: 'row',
295 | gap: 25,
296 | },
297 | place: {
298 | width: 100,
299 | height: 100,
300 | borderRadius: 10,
301 | },
302 | placeSelected: {
303 | borderColor: Colors.grey,
304 | borderWidth: 2,
305 | borderRadius: 10,
306 | width: 100,
307 | height: 100,
308 | },
309 | previewText: {
310 | fontFamily: 'mon-sb',
311 | fontSize: 14,
312 | color: Colors.grey,
313 | },
314 | previewdData: {
315 | fontFamily: 'mon-sb',
316 | fontSize: 14,
317 | color: Colors.dark,
318 | },
319 |
320 | guestItem: {
321 | flexDirection: 'row',
322 | justifyContent: 'space-between',
323 | alignItems: 'center',
324 | paddingVertical: 16,
325 | },
326 | itemBorder: {
327 | borderBottomWidth: StyleSheet.hairlineWidth,
328 | borderBottomColor: Colors.grey,
329 | },
330 | });
331 | export default Page;
332 |
--------------------------------------------------------------------------------
/app/(modals)/login.tsx:
--------------------------------------------------------------------------------
1 | import Colors from '@/constants/Colors';
2 | import { useOAuth } from '@clerk/clerk-expo';
3 | import { Ionicons } from '@expo/vector-icons';
4 | import { useRouter } from 'expo-router';
5 | import { View, StyleSheet, TextInput, Text, TouchableOpacity } from 'react-native';
6 |
7 | // https://github.com/clerkinc/clerk-expo-starter/blob/main/components/OAuth.tsx
8 | import { useWarmUpBrowser } from '@/hooks/useWarmUpBrowser';
9 | import { defaultStyles } from '@/constants/Styles';
10 |
11 | enum Strategy {
12 | Google = 'oauth_google',
13 | Apple = 'oauth_apple',
14 | Facebook = 'oauth_facebook',
15 | }
16 | const Page = () => {
17 | useWarmUpBrowser();
18 |
19 | const router = useRouter();
20 | const { startOAuthFlow: googleAuth } = useOAuth({ strategy: 'oauth_google' });
21 | const { startOAuthFlow: appleAuth } = useOAuth({ strategy: 'oauth_apple' });
22 | const { startOAuthFlow: facebookAuth } = useOAuth({ strategy: 'oauth_facebook' });
23 |
24 | const onSelectAuth = async (strategy: Strategy) => {
25 | const selectedAuth = {
26 | [Strategy.Google]: googleAuth,
27 | [Strategy.Apple]: appleAuth,
28 | [Strategy.Facebook]: facebookAuth,
29 | }[strategy];
30 |
31 | try {
32 | const { createdSessionId, setActive } = await selectedAuth();
33 |
34 | if (createdSessionId) {
35 | setActive!({ session: createdSessionId });
36 | router.back();
37 | }
38 | } catch (err) {
39 | console.error('OAuth error', err);
40 | }
41 | };
42 |
43 | return (
44 |
45 |
50 |
51 |
52 | Continue
53 |
54 |
55 |
56 |
63 | or
64 |
71 |
72 |
73 |
74 |
75 |
76 | Continue with Phone
77 |
78 |
79 | onSelectAuth(Strategy.Apple)}>
80 |
81 | Continue with Apple
82 |
83 |
84 | onSelectAuth(Strategy.Google)}>
85 |
86 | Continue with Google
87 |
88 |
89 | onSelectAuth(Strategy.Facebook)}>
90 |
91 | Continue with Facebook
92 |
93 |
94 |
95 | );
96 | };
97 |
98 | export default Page;
99 |
100 | const styles = StyleSheet.create({
101 | container: {
102 | flex: 1,
103 | backgroundColor: '#fff',
104 | padding: 26,
105 | },
106 |
107 | seperatorView: {
108 | flexDirection: 'row',
109 | gap: 10,
110 | alignItems: 'center',
111 | marginVertical: 30,
112 | },
113 | seperator: {
114 | fontFamily: 'mon-sb',
115 | color: Colors.grey,
116 | fontSize: 16,
117 | },
118 | btnOutline: {
119 | backgroundColor: '#fff',
120 | borderWidth: 1,
121 | borderColor: Colors.grey,
122 | height: 50,
123 | borderRadius: 8,
124 | alignItems: 'center',
125 | justifyContent: 'center',
126 | flexDirection: 'row',
127 | paddingHorizontal: 10,
128 | },
129 | btnOutlineText: {
130 | color: '#000',
131 | fontSize: 16,
132 | fontFamily: 'mon-sb',
133 | },
134 | });
135 |
--------------------------------------------------------------------------------
/app/(tabs)/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { Tabs } from 'expo-router';
2 | import { Ionicons } from '@expo/vector-icons';
3 | import { FontAwesome5 } from '@expo/vector-icons';
4 | import { MaterialCommunityIcons } from '@expo/vector-icons';
5 | import Colors from '@/constants/Colors';
6 |
7 | const Layout = () => {
8 | return (
9 |
16 | ,
21 | }}
22 | />
23 | (
28 |
29 | ),
30 | }}
31 | />
32 | ,
37 | }}
38 | />
39 | (
44 |
45 | ),
46 | }}
47 | />
48 | (
55 |
56 | ),
57 | }}
58 | />
59 |
60 | );
61 | };
62 |
63 | export default Layout;
64 |
--------------------------------------------------------------------------------
/app/(tabs)/inbox.tsx:
--------------------------------------------------------------------------------
1 | import { View, Text } from 'react-native';
2 | import React from 'react';
3 |
4 | const Page = () => {
5 | return (
6 |
7 | Page
8 |
9 | );
10 | };
11 |
12 | export default Page;
13 |
--------------------------------------------------------------------------------
/app/(tabs)/index.tsx:
--------------------------------------------------------------------------------
1 | import { View } from 'react-native';
2 | import React, { useMemo, useState } from 'react';
3 | import ListingsBottomSheet from '@/components/ListingsBottomSheet';
4 | import listingsData from '@/assets/data/airbnb-listings.json';
5 | import ListingsMap from '@/components/ListingsMap';
6 | import listingsDataGeo from '@/assets/data/airbnb-listings.geo.json';
7 | import { Stack } from 'expo-router';
8 | import ExploreHeader from '@/components/ExploreHeader';
9 |
10 | const Page = () => {
11 | const items = useMemo(() => listingsData as any, []);
12 | const getoItems = useMemo(() => listingsDataGeo, []);
13 | const [category, setCategory] = useState('Tiny homes');
14 |
15 | const onDataChanged = (category: string) => {
16 | setCategory(category);
17 | };
18 |
19 | return (
20 |
21 | {/* Define pour custom header */}
22 | ,
25 | }}
26 | />
27 |
28 |
29 |
30 | );
31 | };
32 |
33 | export default Page;
34 |
--------------------------------------------------------------------------------
/app/(tabs)/profile.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | View,
3 | Text,
4 | Button,
5 | StyleSheet,
6 | SafeAreaView,
7 | Image,
8 | TouchableOpacity,
9 | TextInput,
10 | } from 'react-native';
11 | import React, { useEffect, useState } from 'react';
12 | import { useAuth, useUser } from '@clerk/clerk-expo';
13 | import { defaultStyles } from '@/constants/Styles';
14 | import { Ionicons } from '@expo/vector-icons';
15 | import Colors from '@/constants/Colors';
16 | import { Link } from 'expo-router';
17 | import * as ImagePicker from 'expo-image-picker';
18 |
19 | const Page = () => {
20 | const { signOut, isSignedIn } = useAuth();
21 | const { user } = useUser();
22 | const [firstName, setFirstName] = useState(user?.firstName);
23 | const [lastName, setLastName] = useState(user?.lastName);
24 | const [email, setEmail] = useState(user?.emailAddresses[0].emailAddress);
25 | const [edit, setEdit] = useState(false);
26 |
27 | // Load user data on mount
28 | useEffect(() => {
29 | if (!user) {
30 | return;
31 | }
32 |
33 | setFirstName(user.firstName);
34 | setLastName(user.lastName);
35 | setEmail(user.emailAddresses[0].emailAddress);
36 | }, [user]);
37 |
38 | // Update Clerk user data
39 | const onSaveUser = async () => {
40 | try {
41 | await user?.update({
42 | firstName: firstName!,
43 | lastName: lastName!,
44 | });
45 | } catch (error) {
46 | console.log(error);
47 | } finally {
48 | setEdit(false);
49 | }
50 | };
51 |
52 | // Capture image from camera roll
53 | // Upload to Clerk as avatar
54 | const onCaptureImage = async () => {
55 | let result = await ImagePicker.launchImageLibraryAsync({
56 | mediaTypes: ImagePicker.MediaTypeOptions.Images,
57 | allowsEditing: true,
58 | quality: 0.75,
59 | base64: true,
60 | });
61 |
62 | if (!result.canceled) {
63 | const base64 = `data:image/png;base64,${result.assets[0].base64}`;
64 | user?.setProfileImage({
65 | file: base64,
66 | });
67 | }
68 | };
69 |
70 | return (
71 |
72 |
73 | Profile
74 |
75 |
76 |
77 | {user && (
78 |
79 |
80 |
81 |
82 |
83 | {!edit && (
84 |
85 |
86 | {firstName} {lastName}
87 |
88 | setEdit(true)}>
89 |
90 |
91 |
92 | )}
93 | {edit && (
94 |
95 |
101 |
107 |
108 |
109 |
110 |
111 | )}
112 |
113 | {email}
114 | Since {user?.createdAt!.toLocaleDateString()}
115 |
116 | )}
117 |
118 | {isSignedIn &&
125 | );
126 | };
127 |
128 | const styles = StyleSheet.create({
129 | headerContainer: {
130 | flexDirection: 'row',
131 | justifyContent: 'space-between',
132 | padding: 24,
133 | },
134 | header: {
135 | fontFamily: 'mon-b',
136 | fontSize: 24,
137 | },
138 | card: {
139 | backgroundColor: '#fff',
140 | padding: 24,
141 | borderRadius: 16,
142 | marginHorizontal: 24,
143 | marginTop: 24,
144 | elevation: 2,
145 | shadowColor: '#000',
146 | shadowOpacity: 0.2,
147 | shadowRadius: 6,
148 | shadowOffset: {
149 | width: 1,
150 | height: 2,
151 | },
152 | alignItems: 'center',
153 | gap: 14,
154 | marginBottom: 24,
155 | },
156 | avatar: {
157 | width: 100,
158 | height: 100,
159 | borderRadius: 50,
160 | backgroundColor: Colors.grey,
161 | },
162 | editRow: {
163 | flex: 1,
164 | flexDirection: 'row',
165 | alignItems: 'center',
166 | justifyContent: 'center',
167 | gap: 8,
168 | },
169 | });
170 |
171 | export default Page;
172 |
--------------------------------------------------------------------------------
/app/(tabs)/trips.tsx:
--------------------------------------------------------------------------------
1 | import { View, Text } from 'react-native';
2 | import React from 'react';
3 |
4 | const Page = () => {
5 | return (
6 |
7 | Page
8 |
9 | );
10 | };
11 |
12 | export default Page;
13 |
--------------------------------------------------------------------------------
/app/(tabs)/whishlists.tsx:
--------------------------------------------------------------------------------
1 | import { View, Text } from 'react-native';
2 | import React from 'react';
3 |
4 | const Page = () => {
5 | return (
6 |
7 | Page
8 |
9 | );
10 | };
11 |
12 | export default Page;
13 |
--------------------------------------------------------------------------------
/app/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { useFonts } from 'expo-font';
2 | import { SplashScreen, Stack, useRouter } from 'expo-router';
3 | import { useEffect } from 'react';
4 | import { ClerkProvider, useAuth } from '@clerk/clerk-expo';
5 | import * as SecureStore from 'expo-secure-store';
6 | import { Ionicons } from '@expo/vector-icons';
7 | import Colors from '@/constants/Colors';
8 | import ModalHeaderText from '@/components/ModalHeaderText';
9 | import { TouchableOpacity } from 'react-native';
10 |
11 | const CLERK_PUBLISHABLE_KEY = process.env.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY;
12 | // Cache the Clerk JWT
13 | const tokenCache = {
14 | async getToken(key: string) {
15 | try {
16 | return SecureStore.getItemAsync(key);
17 | } catch (err) {
18 | return null;
19 | }
20 | },
21 | async saveToken(key: string, value: string) {
22 | try {
23 | return SecureStore.setItemAsync(key, value);
24 | } catch (err) {
25 | return;
26 | }
27 | },
28 | };
29 |
30 | // Prevent the splash screen from auto-hiding before asset loading is complete.
31 | SplashScreen.preventAutoHideAsync();
32 |
33 | export default function RootLayout() {
34 | const [loaded, error] = useFonts({
35 | mon: require('../assets/fonts/Montserrat-Regular.ttf'),
36 | 'mon-sb': require('../assets/fonts/Montserrat-SemiBold.ttf'),
37 | 'mon-b': require('../assets/fonts/Montserrat-Bold.ttf'),
38 | });
39 |
40 | // Expo Router uses Error Boundaries to catch errors in the navigation tree.
41 | useEffect(() => {
42 | if (error) throw error;
43 | }, [error]);
44 |
45 | useEffect(() => {
46 | if (loaded) {
47 | SplashScreen.hideAsync();
48 | }
49 | }, [loaded]);
50 |
51 | if (!loaded) {
52 | return null;
53 | }
54 |
55 | return (
56 |
57 |
58 |
59 | );
60 | }
61 |
62 | function RootLayoutNav() {
63 | const { isLoaded, isSignedIn } = useAuth();
64 | const router = useRouter();
65 |
66 | // Automatically open login if user is not authenticated
67 | useEffect(() => {
68 | if (isLoaded && !isSignedIn) {
69 | router.push('/(modals)/login');
70 | }
71 | }, [isLoaded]);
72 |
73 | return (
74 |
75 | (
84 | router.back()}>
85 |
86 |
87 | ),
88 | }}
89 | />
90 |
91 |
92 | ,
99 | headerLeft: () => (
100 | router.back()}
102 | style={{
103 | backgroundColor: '#fff',
104 | borderColor: Colors.grey,
105 | borderRadius: 20,
106 | borderWidth: 1,
107 | padding: 4,
108 | }}>
109 |
110 |
111 | ),
112 | }}
113 | />
114 |
115 | );
116 | }
117 |
--------------------------------------------------------------------------------
/app/listing/[id].tsx:
--------------------------------------------------------------------------------
1 | import { useLocalSearchParams, useNavigation } from 'expo-router';
2 | import React, { useLayoutEffect } from 'react';
3 | import { View, Text, StyleSheet, Image, Dimensions, TouchableOpacity, Share } from 'react-native';
4 | import listingsData from '@/assets/data/airbnb-listings.json';
5 | import { Ionicons } from '@expo/vector-icons';
6 | import Colors from '@/constants/Colors';
7 | import Animated, {
8 | SlideInDown,
9 | interpolate,
10 | useAnimatedRef,
11 | useAnimatedStyle,
12 | useScrollViewOffset,
13 | } from 'react-native-reanimated';
14 | import { defaultStyles } from '@/constants/Styles';
15 |
16 | const { width } = Dimensions.get('window');
17 | const IMG_HEIGHT = 300;
18 |
19 | const DetailsPage = () => {
20 | const { id } = useLocalSearchParams();
21 | const listing = (listingsData as any[]).find((item) => item.id === id);
22 | const navigation = useNavigation();
23 | const scrollRef = useAnimatedRef();
24 |
25 | const shareListing = async () => {
26 | try {
27 | await Share.share({
28 | title: listing.name,
29 | url: listing.listing_url,
30 | });
31 | } catch (err) {
32 | console.log(err);
33 | }
34 | };
35 |
36 | useLayoutEffect(() => {
37 | navigation.setOptions({
38 | headerTitle: '',
39 | headerTransparent: true,
40 |
41 | headerBackground: () => (
42 |
43 | ),
44 | headerRight: () => (
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | ),
54 | headerLeft: () => (
55 | navigation.goBack()}>
56 |
57 |
58 | ),
59 | });
60 | }, []);
61 |
62 | const scrollOffset = useScrollViewOffset(scrollRef);
63 |
64 | const imageAnimatedStyle = useAnimatedStyle(() => {
65 | return {
66 | transform: [
67 | {
68 | translateY: interpolate(
69 | scrollOffset.value,
70 | [-IMG_HEIGHT, 0, IMG_HEIGHT, IMG_HEIGHT],
71 | [-IMG_HEIGHT / 2, 0, IMG_HEIGHT * 0.75]
72 | ),
73 | },
74 | {
75 | scale: interpolate(scrollOffset.value, [-IMG_HEIGHT, 0, IMG_HEIGHT], [2, 1, 1]),
76 | },
77 | ],
78 | };
79 | });
80 |
81 | const headerAnimatedStyle = useAnimatedStyle(() => {
82 | return {
83 | opacity: interpolate(scrollOffset.value, [0, IMG_HEIGHT / 1.5], [0, 1]),
84 | };
85 | }, []);
86 |
87 | return (
88 |
89 |
93 |
98 |
99 |
100 | {listing.name}
101 |
102 | {listing.room_type} in {listing.smart_location}
103 |
104 |
105 | {listing.guests_included} guests · {listing.bedrooms} bedrooms · {listing.beds} bed ·{' '}
106 | {listing.bathrooms} bathrooms
107 |
108 |
109 |
110 |
111 | {listing.review_scores_rating / 20} · {listing.number_of_reviews} reviews
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 | Hosted by {listing.host_name}
121 | Host since {listing.host_since}
122 |
123 |
124 |
125 |
126 |
127 | {listing.description}
128 |
129 |
130 |
131 |
132 |
134 |
135 | €{listing.price}
136 | night
137 |
138 |
139 |
140 | Reserve
141 |
142 |
143 |
144 |
145 | );
146 | };
147 |
148 | const styles = StyleSheet.create({
149 | container: {
150 | flex: 1,
151 | backgroundColor: 'white',
152 | },
153 | image: {
154 | height: IMG_HEIGHT,
155 | width: width,
156 | },
157 | infoContainer: {
158 | padding: 24,
159 | backgroundColor: '#fff',
160 | },
161 | name: {
162 | fontSize: 26,
163 | fontWeight: 'bold',
164 | fontFamily: 'mon-sb',
165 | },
166 | location: {
167 | fontSize: 18,
168 | marginTop: 10,
169 | fontFamily: 'mon-sb',
170 | },
171 | rooms: {
172 | fontSize: 16,
173 | color: Colors.grey,
174 | marginVertical: 4,
175 | fontFamily: 'mon',
176 | },
177 | ratings: {
178 | fontSize: 16,
179 | fontFamily: 'mon-sb',
180 | },
181 | divider: {
182 | height: StyleSheet.hairlineWidth,
183 | backgroundColor: Colors.grey,
184 | marginVertical: 16,
185 | },
186 | host: {
187 | width: 50,
188 | height: 50,
189 | borderRadius: 50,
190 | backgroundColor: Colors.grey,
191 | },
192 | hostView: {
193 | flexDirection: 'row',
194 | alignItems: 'center',
195 | gap: 12,
196 | },
197 | footerText: {
198 | height: '100%',
199 | justifyContent: 'center',
200 | flexDirection: 'row',
201 | alignItems: 'center',
202 | gap: 4,
203 | },
204 | footerPrice: {
205 | fontSize: 18,
206 | fontFamily: 'mon-sb',
207 | },
208 | roundButton: {
209 | width: 40,
210 | height: 40,
211 | borderRadius: 50,
212 | backgroundColor: 'white',
213 | alignItems: 'center',
214 | justifyContent: 'center',
215 | color: Colors.primary,
216 | },
217 | bar: {
218 | flexDirection: 'row',
219 | alignItems: 'center',
220 | justifyContent: 'center',
221 | gap: 10,
222 | },
223 | header: {
224 | backgroundColor: '#fff',
225 | height: 100,
226 | borderBottomWidth: StyleSheet.hairlineWidth,
227 | borderColor: Colors.grey,
228 | },
229 |
230 | description: {
231 | fontSize: 16,
232 | marginTop: 10,
233 | fontFamily: 'mon',
234 | },
235 | });
236 |
237 | export default DetailsPage;
238 |
--------------------------------------------------------------------------------
/assets/data/places.ts:
--------------------------------------------------------------------------------
1 | export const places = [
2 | {
3 | title: "I'm flexible",
4 | img: require('@/assets/data/world-0.png'),
5 | },
6 | {
7 | title: 'United States',
8 | img: require('@/assets/data/world-1.png'),
9 | },
10 | {
11 | title: 'Italy',
12 | img: require('@/assets/data/world-2.png'),
13 | },
14 | {
15 | title: 'Middle East',
16 | img: require('@/assets/data/world-3.png'),
17 | },
18 | {
19 | title: 'Netherlands',
20 | img: require('@/assets/data/world-4.png'),
21 | },
22 | {
23 | title: 'Southeast Asia',
24 | img: require('@/assets/data/world-5.png'),
25 | },
26 | ];
27 |
--------------------------------------------------------------------------------
/assets/data/world-0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/airbnb-clone-react-native/e827dc12c8164183ba3267f05d936bd113162d6e/assets/data/world-0.png
--------------------------------------------------------------------------------
/assets/data/world-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/airbnb-clone-react-native/e827dc12c8164183ba3267f05d936bd113162d6e/assets/data/world-1.png
--------------------------------------------------------------------------------
/assets/data/world-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/airbnb-clone-react-native/e827dc12c8164183ba3267f05d936bd113162d6e/assets/data/world-2.png
--------------------------------------------------------------------------------
/assets/data/world-3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/airbnb-clone-react-native/e827dc12c8164183ba3267f05d936bd113162d6e/assets/data/world-3.png
--------------------------------------------------------------------------------
/assets/data/world-4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/airbnb-clone-react-native/e827dc12c8164183ba3267f05d936bd113162d6e/assets/data/world-4.png
--------------------------------------------------------------------------------
/assets/data/world-5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/airbnb-clone-react-native/e827dc12c8164183ba3267f05d936bd113162d6e/assets/data/world-5.png
--------------------------------------------------------------------------------
/assets/fonts/Montserrat-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/airbnb-clone-react-native/e827dc12c8164183ba3267f05d936bd113162d6e/assets/fonts/Montserrat-Bold.ttf
--------------------------------------------------------------------------------
/assets/fonts/Montserrat-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/airbnb-clone-react-native/e827dc12c8164183ba3267f05d936bd113162d6e/assets/fonts/Montserrat-Regular.ttf
--------------------------------------------------------------------------------
/assets/fonts/Montserrat-SemiBold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/airbnb-clone-react-native/e827dc12c8164183ba3267f05d936bd113162d6e/assets/fonts/Montserrat-SemiBold.ttf
--------------------------------------------------------------------------------
/assets/images/adaptive-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/airbnb-clone-react-native/e827dc12c8164183ba3267f05d936bd113162d6e/assets/images/adaptive-icon.png
--------------------------------------------------------------------------------
/assets/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/airbnb-clone-react-native/e827dc12c8164183ba3267f05d936bd113162d6e/assets/images/favicon.png
--------------------------------------------------------------------------------
/assets/images/google.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/assets/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/airbnb-clone-react-native/e827dc12c8164183ba3267f05d936bd113162d6e/assets/images/icon.png
--------------------------------------------------------------------------------
/assets/images/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/airbnb-clone-react-native/e827dc12c8164183ba3267f05d936bd113162d6e/assets/images/splash.png
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = function (api) {
2 | api.cache(true);
3 | return {
4 | presets: ['babel-preset-expo'],
5 | plugins: [
6 | // Required for expo-router
7 | 'expo-router/babel',
8 | 'react-native-reanimated/plugin',
9 | ],
10 | };
11 | };
12 |
--------------------------------------------------------------------------------
/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/airbnb-clone-react-native/e827dc12c8164183ba3267f05d936bd113162d6e/banner.png
--------------------------------------------------------------------------------
/components/ExploreHeader.tsx:
--------------------------------------------------------------------------------
1 | import { View, Text, SafeAreaView, StyleSheet, ScrollView, TouchableOpacity } from 'react-native';
2 | import { useRef, useState } from 'react';
3 | import Colors from '@/constants/Colors';
4 | import { Ionicons } from '@expo/vector-icons';
5 | import { MaterialIcons } from '@expo/vector-icons';
6 | import * as Haptics from 'expo-haptics';
7 | import { Link } from 'expo-router';
8 |
9 | const categories = [
10 | {
11 | name: 'Tiny homes',
12 | icon: 'home',
13 | },
14 | {
15 | name: 'Cabins',
16 | icon: 'house-siding',
17 | },
18 | {
19 | name: 'Trending',
20 | icon: 'local-fire-department',
21 | },
22 | {
23 | name: 'Play',
24 | icon: 'videogame-asset',
25 | },
26 | {
27 | name: 'City',
28 | icon: 'apartment',
29 | },
30 | {
31 | name: 'Beachfront',
32 | icon: 'beach-access',
33 | },
34 | {
35 | name: 'Countryside',
36 | icon: 'nature-people',
37 | },
38 | ];
39 |
40 | interface Props {
41 | onCategoryChanged: (category: string) => void;
42 | }
43 |
44 | const ExploreHeader = ({ onCategoryChanged }: Props) => {
45 | const scrollRef = useRef(null);
46 | const itemsRef = useRef>([]);
47 | const [activeIndex, setActiveIndex] = useState(0);
48 |
49 | const selectCategory = (index: number) => {
50 | const selected = itemsRef.current[index];
51 | setActiveIndex(index);
52 | selected?.measure((x) => {
53 | scrollRef.current?.scrollTo({ x: x - 16, y: 0, animated: true });
54 | });
55 | Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
56 | onCategoryChanged(categories[index].name);
57 | };
58 |
59 | return (
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 | Where to?
69 | Anywhere · Any week
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
88 | {categories.map((item, index) => (
89 | (itemsRef.current[index] = el)}
91 | key={index}
92 | style={activeIndex === index ? styles.categoriesBtnActive : styles.categoriesBtn}
93 | onPress={() => selectCategory(index)}>
94 |
99 |
100 | {item.name}
101 |
102 |
103 | ))}
104 |
105 |
106 |
107 | );
108 | };
109 |
110 | const styles = StyleSheet.create({
111 | container: {
112 | backgroundColor: '#fff',
113 | height: 130,
114 | elevation: 2,
115 | shadowColor: '#000',
116 | shadowOpacity: 0.1,
117 | shadowRadius: 6,
118 | shadowOffset: {
119 | width: 1,
120 | height: 10,
121 | },
122 | },
123 | actionRow: {
124 | flexDirection: 'row',
125 | alignItems: 'center',
126 | justifyContent: 'space-between',
127 | paddingHorizontal: 24,
128 | paddingBottom: 16,
129 | },
130 |
131 | searchBtn: {
132 | backgroundColor: '#fff',
133 | flexDirection: 'row',
134 | gap: 10,
135 | padding: 14,
136 | alignItems: 'center',
137 | width: 280,
138 | borderWidth: StyleSheet.hairlineWidth,
139 | borderColor: '#c2c2c2',
140 | borderRadius: 30,
141 | elevation: 2,
142 | shadowColor: '#000',
143 | shadowOpacity: 0.12,
144 | shadowRadius: 8,
145 | shadowOffset: {
146 | width: 1,
147 | height: 1,
148 | },
149 | },
150 | filterBtn: {
151 | padding: 10,
152 | borderWidth: 1,
153 | borderColor: '#A2A0A2',
154 | borderRadius: 24,
155 | },
156 | categoryText: {
157 | fontSize: 14,
158 | fontFamily: 'mon-sb',
159 | color: Colors.grey,
160 | },
161 | categoryTextActive: {
162 | fontSize: 14,
163 | fontFamily: 'mon-sb',
164 | color: '#000',
165 | },
166 | categoriesBtn: {
167 | flex: 1,
168 | alignItems: 'center',
169 | justifyContent: 'center',
170 | paddingBottom: 8,
171 | },
172 | categoriesBtnActive: {
173 | flex: 1,
174 | alignItems: 'center',
175 | justifyContent: 'center',
176 | borderBottomColor: '#000',
177 | borderBottomWidth: 2,
178 | paddingBottom: 8,
179 | },
180 | });
181 |
182 | export default ExploreHeader;
183 |
--------------------------------------------------------------------------------
/components/Listings.tsx:
--------------------------------------------------------------------------------
1 | import { View, Text, StyleSheet, ListRenderItem, TouchableOpacity } from 'react-native';
2 | import { defaultStyles } from '@/constants/Styles';
3 | import { Ionicons } from '@expo/vector-icons';
4 | import { Link } from 'expo-router';
5 | import Animated, { FadeInRight, FadeOutLeft } from 'react-native-reanimated';
6 | import { useEffect, useRef, useState } from 'react';
7 | import { BottomSheetFlatList, BottomSheetFlatListMethods } from '@gorhom/bottom-sheet';
8 |
9 | interface Props {
10 | listings: any[];
11 | refresh: number;
12 | category: string;
13 | }
14 |
15 | const Listings = ({ listings: items, refresh, category }: Props) => {
16 | const listRef = useRef(null);
17 | const [loading, setLoading] = useState(false);
18 |
19 | // Update the view to scroll the list back top
20 | useEffect(() => {
21 | if (refresh) {
22 | scrollListTop();
23 | }
24 | }, [refresh]);
25 |
26 | const scrollListTop = () => {
27 | listRef.current?.scrollToOffset({ offset: 0, animated: true });
28 | };
29 |
30 | // Use for "updating" the views data after category changed
31 | useEffect(() => {
32 | setLoading(true);
33 |
34 | setTimeout(() => {
35 | setLoading(false);
36 | }, 200);
37 | }, [category]);
38 |
39 | // Render one listing row for the FlatList
40 | const renderRow: ListRenderItem = ({ item }) => (
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | {item.name}
50 |
51 |
52 | {item.review_scores_rating / 20}
53 |
54 |
55 | {item.room_type}
56 |
57 | € {item.price}
58 | night
59 |
60 |
61 |
62 |
63 | );
64 |
65 | return (
66 |
67 | {items.length} homes}
72 | />
73 |
74 | );
75 | };
76 |
77 | const styles = StyleSheet.create({
78 | listing: {
79 | padding: 16,
80 | gap: 10,
81 | marginVertical: 16,
82 | },
83 | image: {
84 | width: '100%',
85 | height: 300,
86 | borderRadius: 10,
87 | },
88 | info: {
89 | textAlign: 'center',
90 | fontFamily: 'mon-sb',
91 | fontSize: 16,
92 | marginTop: 4,
93 | },
94 | });
95 |
96 | export default Listings;
97 |
--------------------------------------------------------------------------------
/components/ListingsBottomSheet.tsx:
--------------------------------------------------------------------------------
1 | import { View, StyleSheet, Text, TouchableOpacity } from 'react-native';
2 | import { useMemo, useRef, useState } from 'react';
3 | import BottomSheet from '@gorhom/bottom-sheet';
4 | import Listings from '@/components/Listings';
5 | import { Ionicons } from '@expo/vector-icons';
6 | import Colors from '@/constants/Colors';
7 |
8 | interface Props {
9 | listings: any[];
10 | category: string;
11 | }
12 |
13 | // Bottom sheet that wraps our Listings component
14 | const ListingsBottomSheet = ({ listings, category }: Props) => {
15 | const snapPoints = useMemo(() => ['10%', '100%'], []);
16 | const bottomSheetRef = useRef(null);
17 | const [refresh, setRefresh] = useState(0);
18 |
19 | const onShowMap = () => {
20 | bottomSheetRef.current?.collapse();
21 | setRefresh(refresh + 1);
22 | };
23 |
24 | return (
25 |
32 |
33 |
34 |
35 |
36 | Map
37 |
38 |
39 |
40 |
41 |
42 | );
43 | };
44 |
45 | const styles = StyleSheet.create({
46 | contentContainer: {
47 | flex: 1,
48 | },
49 | absoluteView: {
50 | position: 'absolute',
51 | bottom: 30,
52 | width: '100%',
53 | alignItems: 'center',
54 | },
55 | btn: {
56 | backgroundColor: Colors.dark,
57 | padding: 14,
58 | height: 50,
59 | borderRadius: 30,
60 | flexDirection: 'row',
61 | marginHorizontal: 'auto',
62 | alignItems: 'center',
63 | },
64 | sheetContainer: {
65 | backgroundColor: '#fff',
66 | elevation: 4,
67 | shadowColor: '#000',
68 | shadowOpacity: 0.3,
69 | shadowRadius: 4,
70 | shadowOffset: {
71 | width: 1,
72 | height: 1,
73 | },
74 | },
75 | });
76 |
77 | export default ListingsBottomSheet;
78 |
--------------------------------------------------------------------------------
/components/ListingsMap.tsx:
--------------------------------------------------------------------------------
1 | import { View, StyleSheet, Text, TouchableOpacity } from 'react-native';
2 | import React, { memo, useEffect, useRef } from 'react';
3 | import { defaultStyles } from '@/constants/Styles';
4 | import { Marker } from 'react-native-maps';
5 | import MapView from 'react-native-map-clustering';
6 | import { useRouter } from 'expo-router';
7 | import { Ionicons } from '@expo/vector-icons';
8 | import Colors from '@/constants/Colors';
9 | import * as Location from 'expo-location';
10 |
11 | interface Props {
12 | listings: any;
13 | }
14 |
15 | const INITIAL_REGION = {
16 | latitude: 37.33,
17 | longitude: -122,
18 | latitudeDelta: 9,
19 | longitudeDelta: 9,
20 | };
21 |
22 | const ListingsMap = memo(({ listings }: Props) => {
23 | const router = useRouter();
24 | const mapRef = useRef(null);
25 |
26 | // When the component mounts, locate the user
27 | useEffect(() => {
28 | onLocateMe();
29 | }, []);
30 |
31 | // When a marker is selected, navigate to the listing page
32 | const onMarkerSelected = (event: any) => {
33 | router.push(`/listing/${event.properties.id}`);
34 | };
35 |
36 | // Focus the map on the user's location
37 | const onLocateMe = async () => {
38 | let { status } = await Location.requestForegroundPermissionsAsync();
39 | if (status !== 'granted') {
40 | return;
41 | }
42 |
43 | let location = await Location.getCurrentPositionAsync({});
44 |
45 | const region = {
46 | latitude: location.coords.latitude,
47 | longitude: location.coords.longitude,
48 | latitudeDelta: 7,
49 | longitudeDelta: 7,
50 | };
51 |
52 | mapRef.current?.animateToRegion(region);
53 | };
54 |
55 | // Overwrite the renderCluster function to customize the cluster markers
56 | const renderCluster = (cluster: any) => {
57 | const { id, geometry, onPress, properties } = cluster;
58 |
59 | const points = properties.point_count;
60 | return (
61 |
68 |
69 |
75 | {points}
76 |
77 |
78 |
79 | );
80 | };
81 |
82 | return (
83 |
84 |
93 | {/* Render all our marker as usual */}
94 | {listings.features.map((item: any) => (
95 | onMarkerSelected(item)}>
102 |
103 | € {item.properties.price}
104 |
105 |
106 | ))}
107 |
108 |
109 |
110 |
111 |
112 | );
113 | });
114 |
115 | const styles = StyleSheet.create({
116 | container: {
117 | flex: 1,
118 | },
119 | marker: {
120 | padding: 8,
121 | alignItems: 'center',
122 | justifyContent: 'center',
123 | backgroundColor: '#fff',
124 | elevation: 5,
125 | borderRadius: 12,
126 | shadowColor: '#000',
127 | shadowOpacity: 0.1,
128 | shadowRadius: 6,
129 | shadowOffset: {
130 | width: 1,
131 | height: 10,
132 | },
133 | },
134 | markerText: {
135 | fontSize: 14,
136 | fontFamily: 'mon-sb',
137 | },
138 | locateBtn: {
139 | position: 'absolute',
140 | top: 70,
141 | right: 20,
142 | backgroundColor: '#fff',
143 | padding: 10,
144 | borderRadius: 10,
145 | elevation: 2,
146 | shadowColor: '#000',
147 | shadowOpacity: 0.1,
148 | shadowRadius: 6,
149 | shadowOffset: {
150 | width: 1,
151 | height: 10,
152 | },
153 | },
154 | });
155 |
156 | export default ListingsMap;
157 |
--------------------------------------------------------------------------------
/components/ModalHeaderText.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { View, TouchableOpacity, Text } from 'react-native';
3 | import Colors from '@/constants/Colors';
4 |
5 | const ModalHeaderText = () => {
6 | const [active, setActive] = useState(0);
7 | return (
8 |
9 | setActive(0)}>
10 |
17 | Stays
18 |
19 |
20 | setActive(1)}>
21 |
28 | Experiences
29 |
30 |
31 |
32 | );
33 | };
34 |
35 | export default ModalHeaderText;
36 |
--------------------------------------------------------------------------------
/constants/Colors.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | primary: '#FF385C',
3 | grey: '#5E5D5E',
4 | dark: '#1A1A1A',
5 | };
6 |
--------------------------------------------------------------------------------
/constants/Styles.ts:
--------------------------------------------------------------------------------
1 | import Colors from '@/constants/Colors';
2 | import { StyleSheet } from 'react-native';
3 |
4 | export const defaultStyles = StyleSheet.create({
5 | container: {
6 | flex: 1,
7 | backgroundColor: '#FDFFFF',
8 | },
9 | inputField: {
10 | height: 44,
11 | borderWidth: 1,
12 | borderColor: '#ABABAB',
13 | borderRadius: 8,
14 | padding: 10,
15 | backgroundColor: '#fff',
16 | },
17 | btn: {
18 | backgroundColor: Colors.primary,
19 | height: 50,
20 | borderRadius: 8,
21 | justifyContent: 'center',
22 | alignItems: 'center',
23 | },
24 | btnText: {
25 | color: '#fff',
26 | fontSize: 16,
27 | fontFamily: 'mon-b',
28 | },
29 | btnIcon: {
30 | position: 'absolute',
31 | left: 16,
32 | },
33 | footer: {
34 | position: 'absolute',
35 | height: 100,
36 | bottom: 0,
37 | left: 0,
38 | right: 0,
39 | backgroundColor: '#fff',
40 | paddingVertical: 10,
41 | paddingHorizontal: 20,
42 | borderTopColor: Colors.grey,
43 | borderTopWidth: StyleSheet.hairlineWidth,
44 | },
45 | });
46 |
--------------------------------------------------------------------------------
/expo-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | // NOTE: This file should not be edited and should be in your git ignore
--------------------------------------------------------------------------------
/hooks/useWarmUpBrowser.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react';
2 | import * as WebBrowser from 'expo-web-browser';
3 |
4 | export const useWarmUpBrowser = () => {
5 | useEffect(() => {
6 | void WebBrowser.warmUpAsync();
7 | return () => {
8 | void WebBrowser.coolDownAsync();
9 | };
10 | }, []);
11 | };
12 |
--------------------------------------------------------------------------------
/metro.config.js:
--------------------------------------------------------------------------------
1 | // Learn more https://docs.expo.io/guides/customizing-metro
2 | const { getDefaultConfig } = require('expo/metro-config');
3 |
4 | /** @type {import('expo/metro-config').MetroConfig} */
5 | const config = getDefaultConfig(__dirname, {
6 | // [Web-only]: Enables CSS support in Metro.
7 | isCSSEnabled: true,
8 | });
9 |
10 | module.exports = config;
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "airbnb",
3 | "main": "expo-router/entry",
4 | "version": "1.0.0",
5 | "scripts": {
6 | "start": "expo start",
7 | "android": "expo start --android",
8 | "ios": "expo start --ios",
9 | "web": "expo start --web",
10 | "test": "jest --watchAll"
11 | },
12 | "jest": {
13 | "preset": "jest-expo"
14 | },
15 | "dependencies": {
16 | "@clerk/clerk-expo": "^0.19.7",
17 | "@expo/vector-icons": "^13.0.0",
18 | "@gorhom/bottom-sheet": "^4.5.1",
19 | "@react-navigation/native": "^6.0.2",
20 | "expo": "~49.0.11",
21 | "expo-blur": "~12.4.1",
22 | "expo-font": "~11.4.0",
23 | "expo-haptics": "~12.4.0",
24 | "expo-linking": "~5.0.2",
25 | "expo-location": "~16.1.0",
26 | "expo-router": "^2.0.9",
27 | "expo-secure-store": "~12.3.1",
28 | "expo-splash-screen": "~0.20.5",
29 | "expo-status-bar": "~1.6.0",
30 | "expo-system-ui": "~2.4.0",
31 | "expo-web-browser": "~12.3.2",
32 | "react": "18.2.0",
33 | "react-dom": "18.2.0",
34 | "react-native": "0.72.4",
35 | "react-native-gesture-handler": "~2.12.0",
36 | "react-native-map-clustering": "^3.4.2",
37 | "react-native-maps": "1.7.1",
38 | "react-native-modern-datepicker": "^1.0.0-beta.91",
39 | "react-native-reanimated": "3.3.x",
40 | "react-native-safe-area-context": "4.6.3",
41 | "react-native-screens": "~3.22.0",
42 | "react-native-web": "~0.19.6",
43 | "expo-image-picker": "~14.3.2"
44 | },
45 | "devDependencies": {
46 | "@babel/core": "^7.20.0",
47 | "@types/react": "~18.2.14",
48 | "@types/react-native": "^0.72.3",
49 | "jest": "^29.2.1",
50 | "jest-expo": "~49.0.0",
51 | "react-test-renderer": "18.2.0",
52 | "typescript": "^5.1.3"
53 | },
54 | "overrides": {
55 | "react-refresh": "~0.14.0"
56 | },
57 | "resolutions": {
58 | "react-refresh": "~0.14.0"
59 | },
60 | "private": true
61 | }
62 |
--------------------------------------------------------------------------------
/screenshots/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/airbnb-clone-react-native/e827dc12c8164183ba3267f05d936bd113162d6e/screenshots/1.png
--------------------------------------------------------------------------------
/screenshots/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/airbnb-clone-react-native/e827dc12c8164183ba3267f05d936bd113162d6e/screenshots/2.png
--------------------------------------------------------------------------------
/screenshots/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/airbnb-clone-react-native/e827dc12c8164183ba3267f05d936bd113162d6e/screenshots/3.png
--------------------------------------------------------------------------------
/screenshots/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/airbnb-clone-react-native/e827dc12c8164183ba3267f05d936bd113162d6e/screenshots/4.png
--------------------------------------------------------------------------------
/screenshots/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/airbnb-clone-react-native/e827dc12c8164183ba3267f05d936bd113162d6e/screenshots/5.png
--------------------------------------------------------------------------------
/screenshots/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Galaxies-dev/airbnb-clone-react-native/e827dc12c8164183ba3267f05d936bd113162d6e/screenshots/demo.gif
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true,
5 | "baseUrl": ".",
6 | "paths": {
7 | "@/*": ["./*"]
8 | }
9 | },
10 | "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
11 | }
12 |
--------------------------------------------------------------------------------