├── assets
├── images
│ ├── icon.png
│ ├── favicon.png
│ ├── icon-64.png
│ ├── splash.png
│ ├── react-logo.png
│ ├── splash-icon.png
│ ├── splash-icon1.png
│ ├── react-logo@2x.png
│ ├── react-logo@3x.png
│ ├── adaptive-icon1 (2).png
│ └── partial-react-logo.png
└── fonts
│ └── SpaceMono-Regular.ttf
├── app
├── _layout.tsx
├── index.tsx
└── (tab)
│ ├── list.tsx
│ └── map.tsx
├── tsconfig.json
├── utils
├── types.ts
└── mock.tsx
├── .gitignore
├── app.json
├── package.json
└── README.md
/assets/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey531/app-uber-scraping/HEAD/assets/images/icon.png
--------------------------------------------------------------------------------
/assets/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey531/app-uber-scraping/HEAD/assets/images/favicon.png
--------------------------------------------------------------------------------
/assets/images/icon-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey531/app-uber-scraping/HEAD/assets/images/icon-64.png
--------------------------------------------------------------------------------
/assets/images/splash.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey531/app-uber-scraping/HEAD/assets/images/splash.png
--------------------------------------------------------------------------------
/assets/images/react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey531/app-uber-scraping/HEAD/assets/images/react-logo.png
--------------------------------------------------------------------------------
/assets/images/splash-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey531/app-uber-scraping/HEAD/assets/images/splash-icon.png
--------------------------------------------------------------------------------
/assets/images/splash-icon1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey531/app-uber-scraping/HEAD/assets/images/splash-icon1.png
--------------------------------------------------------------------------------
/assets/images/react-logo@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey531/app-uber-scraping/HEAD/assets/images/react-logo@2x.png
--------------------------------------------------------------------------------
/assets/images/react-logo@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey531/app-uber-scraping/HEAD/assets/images/react-logo@3x.png
--------------------------------------------------------------------------------
/assets/fonts/SpaceMono-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey531/app-uber-scraping/HEAD/assets/fonts/SpaceMono-Regular.ttf
--------------------------------------------------------------------------------
/app/_layout.tsx:
--------------------------------------------------------------------------------
1 | import { Stack } from "expo-router";
2 |
3 | export default function RootLayout() {
4 | return ;
5 | }
6 |
--------------------------------------------------------------------------------
/assets/images/adaptive-icon1 (2).png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey531/app-uber-scraping/HEAD/assets/images/adaptive-icon1 (2).png
--------------------------------------------------------------------------------
/assets/images/partial-react-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/monkey531/app-uber-scraping/HEAD/assets/images/partial-react-logo.png
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "expo/tsconfig.base",
3 | "compilerOptions": {
4 | "strict": true,
5 | "paths": {
6 | "@/*": [
7 | "./*"
8 | ]
9 | }
10 | },
11 | "include": [
12 | "**/*.ts",
13 | "**/*.tsx"
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/app/index.tsx:
--------------------------------------------------------------------------------
1 | import { View } from 'react-native'
2 | import { Redirect } from 'expo-router';
3 | import { Image } from 'expo-image';
4 |
5 | const PlaceholderImage = require('@/assets/images/splash.png');
6 |
7 | export default function Index() {
8 | return (
9 | <>
10 |
11 |
12 |
13 |
14 | >
15 | );
16 | }
--------------------------------------------------------------------------------
/utils/types.ts:
--------------------------------------------------------------------------------
1 | export enum RequestStatus {
2 | UNCONFIRMED = 'UNCONFIRMED',
3 | CONFIRMED = 'CONFIRMED',
4 | COMPLETED = 'COMPLETED',
5 | }
6 |
7 | export interface Location {
8 | latitude: number;
9 | longitude: number;
10 | }
11 |
12 | export interface RequestData {
13 | id: string;
14 | guestName: string;
15 | pickupAddress: string;
16 | dropoffAddress: string;
17 | requestTime: Date;
18 | phoneNumber: string;
19 | status: RequestStatus;
20 | location: {
21 | pickup: Location;
22 | dropoff: Location;
23 | };
24 | }
--------------------------------------------------------------------------------
/.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 | expo-env.d.ts
11 |
12 | # Native
13 | *.orig.*
14 | *.jks
15 | *.p8
16 | *.p12
17 | *.key
18 | *.mobileprovision
19 |
20 | # Metro
21 | .metro-health-check*
22 |
23 | # debug
24 | npm-debug.*
25 | yarn-debug.*
26 | yarn-error.*
27 |
28 | # macOS
29 | .DS_Store
30 | *.pem
31 |
32 | # local env files
33 | .env*.local
34 |
35 | # typescript
36 | *.tsbuildinfo
37 |
38 | app-example
39 |
--------------------------------------------------------------------------------
/app.json:
--------------------------------------------------------------------------------
1 | {
2 | "expo": {
3 | "name": "uber-clone",
4 | "slug": "uber-clone",
5 | "version": "1.0.0",
6 | "orientation": "portrait",
7 | "scheme": "UberClone",
8 | "icon": "./assets/images/icon.png",
9 | "userInterfaceStyle": "light",
10 | "newArchEnabled": true,
11 | "splash": {
12 | "image": "./assets/images/splash.png",
13 | "resizeMode": "cover",
14 | "backgroundColor": "#ffffff"
15 | },
16 | "assetBundlePatterns": [
17 | "**/*"
18 | ],
19 | "ios": {
20 | "supportsTablet": true,
21 | "bundleIdentifier": "com.uberclone",
22 | "config": {
23 | "googleMapsApiKey": "AIzaSyD7VnZ8xE15tj1VPZkPdxYhs51KxItnHUk"
24 | }
25 | },
26 | "android": {
27 | "adaptiveIcon": {
28 | "foregroundImage": "./assets/images/adaptive-icon.png",
29 | "backgroundColor": "#ffffff"
30 | },
31 | "package": "com.uberclone",
32 | "config": {
33 | "googleMaps": {
34 | "apiKey": "AIzaSyD7VnZ8xE15tj1VPZkPdxYhs51KxItnHUk"
35 | }
36 | },
37 | "permissions": [
38 | "ACCESS_COARSE_LOCATION",
39 | "ACCESS_FINE_LOCATION"
40 | ]
41 | },
42 | "web": {
43 | "favicon": "./assets/images/splash-icon.png"
44 | },
45 | "plugins": [
46 | [
47 | "expo-location",
48 | {
49 | "locationAlwaysAndWhenInUsePermission": "Allow $(PRODUCT_NAME) to use your location.",
50 | "locationAlwaysPermission": "Allow $(PRODUCT_NAME) to use your location.",
51 | "locationWhenInUsePermission": "Allow $(PRODUCT_NAME) to use your location.",
52 | "isIosBackgroundLocationEnabled": true,
53 | "isAndroidBackgroundLocationEnabled": true
54 | }
55 | ]
56 | ]
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "uber-clone",
3 | "main": "expo-router/entry",
4 | "version": "1.0.0",
5 | "scripts": {
6 | "start": "expo start",
7 | "reset-project": "node ./scripts/reset-project.js",
8 | "android": "expo start --android",
9 | "ios": "expo start --ios",
10 | "web": "expo start --web",
11 | "test": "jest --watchAll",
12 | "lint": "expo lint"
13 | },
14 | "jest": {
15 | "preset": "jest-expo"
16 | },
17 | "dependencies": {
18 | "@expo/vector-icons": "^14.0.2",
19 | "@react-navigation/bottom-tabs": "^7.2.0",
20 | "@react-navigation/native": "^7.0.14",
21 | "expo": "~52.0.41",
22 | "expo-blur": "~14.0.3",
23 | "expo-constants": "~17.0.8",
24 | "expo-font": "~13.0.4",
25 | "expo-haptics": "~14.0.1",
26 | "expo-image": "^2.0.7",
27 | "expo-linking": "~7.0.5",
28 | "expo-location": "~18.0.8",
29 | "expo-router": "~4.0.19",
30 | "expo-splash-screen": "~0.29.22",
31 | "expo-status-bar": "~2.0.1",
32 | "expo-symbols": "~0.2.2",
33 | "expo-system-ui": "~4.0.8",
34 | "expo-web-browser": "~14.0.2",
35 | "react": "18.3.1",
36 | "react-dom": "18.3.1",
37 | "react-native": "0.76.7",
38 | "react-native-gesture-handler": "~2.20.2",
39 | "react-native-maps": "1.18.0",
40 | "react-native-reanimated": "~3.16.1",
41 | "react-native-safe-area-context": "4.12.0",
42 | "react-native-screens": "~4.4.0",
43 | "react-native-web": "~0.19.13",
44 | "react-native-webview": "13.12.5"
45 | },
46 | "devDependencies": {
47 | "@babel/core": "^7.25.2",
48 | "@types/jest": "^29.5.12",
49 | "@types/react": "~18.3.12",
50 | "@types/react-test-renderer": "^18.3.0",
51 | "jest": "~29.7.0",
52 | "jest-expo": "^52.0.0",
53 | "react-test-renderer": "18.3.1",
54 | "typescript": "^5.3.3"
55 | },
56 | "private": true
57 | }
58 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Welcome to your Expo app 👋
2 |
3 | This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
4 |
5 | ## Get started
6 |
7 | 1. Install dependencies
8 |
9 | ```bash
10 | npm install
11 | ```
12 |
13 | 2. Start the app
14 |
15 | ```bash
16 | npx expo start
17 | ```
18 |
19 | In the output, you'll find options to open the app in a
20 |
21 | - [development build](https://docs.expo.dev/develop/development-builds/introduction/)
22 | - [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
23 | - [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
24 | - [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
25 |
26 | You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
27 |
28 | ## Get a fresh project
29 |
30 | When you're ready, run:
31 |
32 | ```bash
33 | npm run reset-project
34 | ```
35 |
36 | This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
37 |
38 | ## Learn more
39 |
40 | To learn more about developing your project with Expo, look at the following resources:
41 |
42 | - [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
43 | - [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
44 |
45 | ## Join the community
46 |
47 | Join our community of developers creating universal apps.
48 |
49 | - [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
50 | - [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
51 |
--------------------------------------------------------------------------------
/utils/mock.tsx:
--------------------------------------------------------------------------------
1 | import { RequestStatus } from './types';
2 |
3 | export interface RequestData {
4 | id: string;
5 | guestName: string;
6 | pickupAddress: string;
7 | dropoffAddress: string;
8 | requestTime: Date;
9 | phoneNumber: string;
10 | status: RequestStatus;
11 | location: {
12 | pickup: {
13 | latitude: number;
14 | longitude: number;
15 | };
16 | dropoff: {
17 | latitude: number;
18 | longitude: number;
19 | };
20 | };
21 | }
22 |
23 | // New York City coordinates boundaries
24 | const NYC_BOUNDS = {
25 | latitude: {
26 | min: 40.7,
27 | max: 40.8,
28 | },
29 | longitude: {
30 | min: -74.02,
31 | max: -73.92,
32 | }
33 | };
34 |
35 | const getRandomCoordinate = (min: number, max: number) => {
36 | return min + Math.random() * (max - min);
37 | };
38 |
39 | const getRandomLocation = () => {
40 | return {
41 | latitude: getRandomCoordinate(NYC_BOUNDS.latitude.min, NYC_BOUNDS.latitude.max),
42 | longitude: getRandomCoordinate(NYC_BOUNDS.longitude.min, NYC_BOUNDS.longitude.max),
43 | };
44 | };
45 |
46 | export const generateMockRequests = (count: number): RequestData[] => {
47 | const requests: RequestData[] = [];
48 | const now = new Date();
49 |
50 | for (let i = 0; i < count; i++) {
51 | const pickupLocation = getRandomLocation();
52 | const dropoffLocation = getRandomLocation();
53 | const status = Math.random() < 0.4
54 | ? RequestStatus.UNCONFIRMED
55 | : Math.random() < 0.7
56 | ? RequestStatus.CONFIRMED
57 | : RequestStatus.COMPLETED;
58 |
59 | requests.push({
60 | id: `request-${i}`,
61 | guestName: `Guest ${i + 1}`,
62 | pickupAddress: `${pickupLocation.latitude.toFixed(4)}, ${pickupLocation.longitude.toFixed(4)}`,
63 | dropoffAddress: `${dropoffLocation.latitude.toFixed(4)}, ${dropoffLocation.longitude.toFixed(4)}`,
64 | requestTime: new Date(now.getTime() - Math.random() * 24 * 60 * 60 * 1000),
65 | phoneNumber: `+1${Math.floor(Math.random() * 1000000000).toString().padStart(10, '0')}`,
66 | status,
67 | location: {
68 | pickup: pickupLocation,
69 | dropoff: dropoffLocation,
70 | },
71 | });
72 | }
73 |
74 | return requests.sort((a, b) => b.requestTime.getTime() - a.requestTime.getTime());
75 | };
76 |
77 | export const mockRequests = generateMockRequests(20);
--------------------------------------------------------------------------------
/app/(tab)/list.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef } from 'react';
2 | import { View, Text, StyleSheet, ScrollView, TouchableOpacity, TextInput, Pressable, Animated, Dimensions } from 'react-native';
3 | import { Ionicons } from '@expo/vector-icons';
4 | import { mockRequests } from '@/utils/mock';
5 | import { RequestStatus, RequestData } from '@/utils/types';
6 | import { Link, router } from 'expo-router';
7 |
8 | const { width } = Dimensions.get('window');
9 |
10 | export default function ListScreen() {
11 | const [requests, setRequests] = useState(mockRequests);
12 | const [searchQuery, setSearchQuery] = useState('');
13 | const [showUnconfirmedOnly, setShowUnconfirmedOnly] = useState(false);
14 | const fadeAnim = useRef(new Animated.Value(0)).current;
15 | const scaleAnim = useRef(new Animated.Value(1)).current;
16 |
17 | const unconfirmedCount = requests.filter(r => r.status === RequestStatus.UNCONFIRMED).length;
18 |
19 | const filteredRequests = requests.filter(request => {
20 | const matchesSearch = request.guestName.toLowerCase().includes(searchQuery.toLowerCase()) ||
21 | request.pickupAddress.toLowerCase().includes(searchQuery.toLowerCase()) ||
22 | request.dropoffAddress.toLowerCase().includes(searchQuery.toLowerCase());
23 |
24 | return matchesSearch && (!showUnconfirmedOnly || request.status === RequestStatus.UNCONFIRMED);
25 | });
26 |
27 | const handleRequestPress = (requestId: string) => {
28 | // Animate the press
29 | Animated.sequence([
30 | Animated.timing(scaleAnim, {
31 | toValue: 0.95,
32 | duration: 100,
33 | useNativeDriver: true,
34 | }),
35 | Animated.timing(scaleAnim, {
36 | toValue: 1,
37 | duration: 100,
38 | useNativeDriver: true,
39 | }),
40 | ]).start();
41 |
42 | setRequests(prev => prev.map(request =>
43 | request.id === requestId && request.status === RequestStatus.UNCONFIRMED
44 | ? { ...request, status: RequestStatus.CONFIRMED }
45 | : request
46 | ));
47 | };
48 |
49 | const refreshData = () => {
50 | // Animate refresh icon
51 | Animated.sequence([
52 | Animated.timing(scaleAnim, {
53 | toValue: 1.2,
54 | duration: 200,
55 | useNativeDriver: true,
56 | }),
57 | Animated.timing(scaleAnim, {
58 | toValue: 1,
59 | duration: 200,
60 | useNativeDriver: true,
61 | }),
62 | ]).start();
63 |
64 | setRequests(mockRequests);
65 | };
66 |
67 | const getBackgroundColor = (status: RequestStatus) => {
68 | switch (status) {
69 | case RequestStatus.UNCONFIRMED:
70 | return '#e8f5e9';
71 | case RequestStatus.CONFIRMED:
72 | return '#ffebee';
73 | case RequestStatus.COMPLETED:
74 | return '#f5f5f5';
75 | }
76 | };
77 |
78 | const handleViewOnMap = (request: RequestData) => {
79 | // Animate the press
80 | Animated.sequence([
81 | Animated.timing(scaleAnim, {
82 | toValue: 0.95,
83 | duration: 100,
84 | useNativeDriver: true,
85 | }),
86 | Animated.timing(scaleAnim, {
87 | toValue: 1,
88 | duration: 100,
89 | useNativeDriver: true,
90 | }),
91 | ]).start();
92 |
93 | // Navigate to map with the request ID
94 | router.push({
95 | pathname: '/map',
96 | params: { selectedRequestId: request.id }
97 | });
98 | };
99 |
100 | // Fade in animation on mount
101 | React.useEffect(() => {
102 | Animated.timing(fadeAnim, {
103 | toValue: 1,
104 | duration: 500,
105 | useNativeDriver: true,
106 | }).start();
107 | }, []);
108 |
109 | return (
110 | <>
111 |
112 |
113 | {/***** Search *****/}
114 |
115 |
116 |
123 |
124 |
125 | {/***** Notification *****/}
126 | setShowUnconfirmedOnly(!showUnconfirmedOnly)}
129 | >
130 |
131 | {unconfirmedCount > 0 && (
132 |
140 | {unconfirmedCount}
141 |
142 | )}
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 | {/***** Main Section *****/}
152 |
157 | {filteredRequests.map((request, index) => (
158 |
173 | handleRequestPress(request.id)}
179 | >
180 | {request.guestName}
181 |
182 | {/***** Position *****/}
183 |
184 |
185 |
186 | {request.pickupAddress}
187 |
188 |
189 |
190 | {request.dropoffAddress}
191 |
192 |
193 |
194 | {request.status === RequestStatus.CONFIRMED && (
195 |
203 |
204 |
205 | )}
206 |
207 |
208 |
209 |
210 | {request.requestTime.toLocaleTimeString()}
211 |
212 |
213 |
214 |
215 | {request.phoneNumber}
216 |
217 | {request.status !== RequestStatus.COMPLETED && (
218 | handleViewOnMap(request)}
221 | >
222 |
223 | Map
224 |
225 | )}
226 |
227 |
228 |
229 | ))}
230 |
231 |
232 |
233 | {/***** Tab Section *****/}
234 |
235 |
236 | List
237 |
238 |
239 |
240 | Map
241 |
242 |
243 |
244 | >
245 | );
246 | }
247 |
248 | const styles = StyleSheet.create({
249 | container: {
250 | flex: 1,
251 | backgroundColor: '#fff',
252 | },
253 | header: {
254 | flexDirection: 'row',
255 | alignItems: 'center',
256 | paddingHorizontal: 16,
257 | paddingVertical: 8,
258 | },
259 | searchContainer: {
260 | flex: 1,
261 | flexDirection: 'row',
262 | alignItems: 'center',
263 | backgroundColor: '#f5f5f5',
264 | borderRadius: 12,
265 | paddingHorizontal: 8,
266 | elevation: 2,
267 | shadowColor: '#000',
268 | shadowOffset: { width: 0, height: 1 },
269 | shadowOpacity: 0.1,
270 | shadowRadius: 2,
271 | },
272 | searchIcon: {
273 | marginRight: 8,
274 | },
275 | searchInput: {
276 | flex: 1,
277 | height: 44,
278 | fontSize: 16,
279 | color: '#333',
280 | },
281 | iconButton: {
282 | padding: 8,
283 | marginLeft: 8,
284 | position: 'relative',
285 | backgroundColor: '#f5f5f5',
286 | borderRadius: 12,
287 | elevation: 2,
288 | shadowColor: '#000',
289 | shadowOffset: { width: 0, height: 1 },
290 | shadowOpacity: 0.1,
291 | shadowRadius: 2,
292 | },
293 | badge: {
294 | position: 'absolute',
295 | top: 0,
296 | right: 0,
297 | backgroundColor: '#f44336',
298 | borderRadius: 10,
299 | minWidth: 20,
300 | height: 20,
301 | justifyContent: 'center',
302 | alignItems: 'center',
303 | elevation: 2,
304 | shadowColor: '#000',
305 | shadowOffset: { width: 0, height: 1 },
306 | shadowOpacity: 0.2,
307 | shadowRadius: 2,
308 | },
309 | badgeText: {
310 | color: '#fff',
311 | fontSize: 12,
312 | fontWeight: 'bold',
313 | },
314 | requestList: {
315 | flex: 1,
316 | },
317 | scrollContent: {
318 | paddingBottom: 16,
319 | },
320 | requestItemContainer: {
321 | marginBottom: 12,
322 | },
323 | requestItem: {
324 | padding: 16,
325 | marginHorizontal: 16,
326 | borderRadius: 16,
327 | elevation: 3,
328 | shadowColor: '#000',
329 | shadowOffset: { width: 0, height: 2 },
330 | shadowOpacity: 0.1,
331 | shadowRadius: 4,
332 | },
333 | guestName: {
334 | fontSize: 20,
335 | fontWeight: 'bold',
336 | marginBottom: 8,
337 | color: '#1a237e',
338 | },
339 | requestDetails: {
340 | gap: 8,
341 | },
342 | addressContainer: {
343 | flex: 1,
344 | flexDirection: 'row',
345 | alignItems: 'center',
346 | gap: 8,
347 | },
348 | address: {
349 | flex: 1,
350 | fontSize: 14,
351 | color: '#333',
352 | },
353 | infoRow: {
354 | flex: 1,
355 | flexDirection: 'row',
356 | justifyContent: 'space-between',
357 | marginTop: 8,
358 | },
359 | infoItem: {
360 | flexDirection: 'row',
361 | alignItems: 'center',
362 | gap: 4,
363 | },
364 | infoText: {
365 | fontSize: 14,
366 | color: '#666',
367 | },
368 | checkmarkContainer: {
369 | position: 'absolute',
370 | top: 16,
371 | right: 16,
372 | backgroundColor: 'white',
373 | borderRadius: 12,
374 | padding: 4,
375 | elevation: 2,
376 | shadowColor: '#000',
377 | shadowOffset: { width: 0, height: 1 },
378 | shadowOpacity: 0.2,
379 | shadowRadius: 2,
380 | },
381 | checkmark: {
382 | transform: [{ scale: 1.2 }],
383 | },
384 | tabBar: {
385 | flexDirection: 'row',
386 | backgroundColor: 'white',
387 | borderTopWidth: 1,
388 | borderTopColor: '#e0e0e0',
389 | elevation: 4,
390 | shadowColor: '#000',
391 | shadowOffset: { width: 0, height: -2 },
392 | shadowOpacity: 0.1,
393 | shadowRadius: 4,
394 | },
395 | tab: {
396 | flex: 1,
397 | paddingVertical: 16,
398 | alignItems: 'center',
399 | borderRadius: 8,
400 | margin: 4,
401 | },
402 | activeTab: {
403 | backgroundColor: '#4CAF50',
404 | },
405 | tabText: {
406 | fontSize: 16,
407 | color: '#666',
408 | fontWeight: '500',
409 | },
410 | activeTabText: {
411 | fontSize: 16,
412 | color: '#fff',
413 | fontWeight: 'bold',
414 | },
415 | bottomRow: {
416 | flexDirection: 'row',
417 | justifyContent: 'space-between',
418 | alignItems: 'center',
419 | marginTop: 12,
420 | },
421 | phoneContainer: {
422 | flexDirection: 'row',
423 | alignItems: 'center',
424 | gap: 4,
425 | },
426 | phoneText: {
427 | fontSize: 14,
428 | color: '#666',
429 | },
430 | mapButton: {
431 | flexDirection: 'row',
432 | alignItems: 'center',
433 | backgroundColor: '#1a232e',
434 | paddingHorizontal: 8,
435 | paddingVertical: 4,
436 | borderRadius: 16,
437 | elevation: 2,
438 | shadowColor: '#000',
439 | shadowOffset: { width: 0, height: 1 },
440 | shadowOpacity: 0.2,
441 | shadowRadius: 2,
442 | },
443 | mapButtonText: {
444 | color: '#fff',
445 | marginLeft: 4,
446 | fontSize: 12,
447 | fontWeight: '600',
448 | },
449 | });
450 |
--------------------------------------------------------------------------------
/app/(tab)/map.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react';
2 | import { View, Text, StyleSheet, Pressable, Animated, Alert, Dimensions } from 'react-native';
3 | import MapView, { Marker, PROVIDER_GOOGLE } from 'react-native-maps';
4 | import { Ionicons } from '@expo/vector-icons';
5 | import { mockRequests } from '@/utils/mock';
6 | import { RequestStatus, RequestData } from '@/utils/types';
7 | import { Link, useLocalSearchParams } from 'expo-router';
8 | import * as Location from 'expo-location';
9 |
10 | const { width, height } = Dimensions.get('window');
11 |
12 | // Default region is NYC, but will be updated with actual location
13 | const INITIAL_REGION = {
14 | latitude: 40.75,
15 | longitude: -73.97,
16 | latitudeDelta: 0,
17 | longitudeDelta: 0,
18 | };
19 |
20 | export default function MapScreen() {
21 | const { selectedRequestId } = useLocalSearchParams<{ selectedRequestId: string }>();
22 | const [requests, setRequests] = useState(mockRequests);
23 | const [selectedRequest, setSelectedRequest] = useState(null);
24 | const markerScale = useRef(new Animated.Value(1)).current;
25 | const [currentLocation, setCurrentLocation] = useState(null);
26 | const [errorMsg, setErrorMsg] = useState(null);
27 | const mapRef = useRef(null);
28 | const slideAnim = useRef(new Animated.Value(0)).current;
29 | const fadeAnim = useRef(new Animated.Value(0)).current;
30 |
31 | // Request and track location
32 | useEffect(() => {
33 | (async () => {
34 | try {
35 | // Request permissions
36 | let { status } = await Location.requestForegroundPermissionsAsync();
37 | if (status !== 'granted') {
38 | setErrorMsg('Permission to access location was denied');
39 | Alert.alert(
40 | 'Location Permission Required',
41 | 'Please enable location services to use this feature.',
42 | [{ text: 'OK' }]
43 | );
44 | return;
45 | }
46 |
47 | // Get initial location
48 | let location = await Location.getCurrentPositionAsync({
49 | accuracy: Location.Accuracy.Balanced,
50 | });
51 | setCurrentLocation(location);
52 |
53 | // Center map on current location
54 | if (mapRef.current && location) {
55 | mapRef.current.animateToRegion({
56 | latitude: location.coords.latitude,
57 | longitude: location.coords.longitude,
58 | latitudeDelta: 0.05,
59 | longitudeDelta: 0.05,
60 | });
61 | }
62 |
63 | // Start location updates
64 | await Location.watchPositionAsync(
65 | {
66 | accuracy: Location.Accuracy.Balanced,
67 | timeInterval: 5000,
68 | distanceInterval: 10,
69 | },
70 | (newLocation) => {
71 | setCurrentLocation(newLocation);
72 | }
73 | );
74 | } catch (error) {
75 | setErrorMsg('Error getting location');
76 | console.error('Location error:', error);
77 | }
78 | })();
79 | }, []);
80 |
81 | // Handle selected request from navigation
82 | useEffect(() => {
83 | if (selectedRequestId) {
84 | const request = requests.find(r => r.id === selectedRequestId);
85 | if (request) {
86 | setSelectedRequest(request);
87 | // Center map on the selected request
88 | if (mapRef.current) {
89 | mapRef.current.animateToRegion({
90 | latitude: request.location.pickup.latitude,
91 | longitude: request.location.pickup.longitude,
92 | latitudeDelta: 0.05,
93 | longitudeDelta: 0.05,
94 | });
95 | }
96 | }
97 | }
98 | }, [selectedRequestId, requests]);
99 |
100 | // Animation for selected marker
101 | useEffect(() => {
102 | if (selectedRequest) {
103 | Animated.loop(
104 | Animated.sequence([
105 | Animated.timing(markerScale, {
106 | toValue: 1.2,
107 | duration: 1000,
108 | useNativeDriver: true,
109 | }),
110 | Animated.timing(markerScale, {
111 | toValue: 1,
112 | duration: 1000,
113 | useNativeDriver: true,
114 | }),
115 | ])
116 | ).start();
117 |
118 | // Animate request info card
119 | Animated.parallel([
120 | Animated.timing(slideAnim, {
121 | toValue: 1,
122 | duration: 300,
123 | useNativeDriver: true,
124 | }),
125 | Animated.timing(fadeAnim, {
126 | toValue: 1,
127 | duration: 300,
128 | useNativeDriver: true,
129 | }),
130 | ]).start();
131 | } else {
132 | markerScale.setValue(1);
133 | // Reset animations
134 | Animated.parallel([
135 | Animated.timing(slideAnim, {
136 | toValue: 0,
137 | duration: 300,
138 | useNativeDriver: true,
139 | }),
140 | Animated.timing(fadeAnim, {
141 | toValue: 0,
142 | duration: 300,
143 | useNativeDriver: true,
144 | }),
145 | ]).start();
146 | }
147 | }, [selectedRequest]);
148 |
149 | const filteredRequests = requests.filter(
150 | request => request.status !== RequestStatus.COMPLETED
151 | );
152 |
153 | const getMarkerColor = (status: RequestStatus) => {
154 | switch (status) {
155 | case RequestStatus.UNCONFIRMED:
156 | return 'green';
157 | case RequestStatus.CONFIRMED:
158 | return '#1a237e';
159 | default:
160 | return 'gray';
161 | }
162 | };
163 |
164 | return (
165 | <>
166 |
167 |
255 | {/* Current location marker with accuracy circle */}
256 | {currentLocation && (
257 |
263 |
264 |
265 |
273 |
274 |
275 | )}
276 |
277 | {/* Request markers */}
278 | {filteredRequests.map((request) => (
279 |
280 | setSelectedRequest(request)}
283 | >
284 |
299 |
300 |
305 |
306 |
307 |
308 |
309 | {selectedRequest?.id === request.id && (
310 |
311 |
319 |
320 |
325 |
326 |
327 |
328 | )}
329 |
330 | ))}
331 |
332 |
333 | {errorMsg && (
334 |
348 | {errorMsg}
349 |
350 | )}
351 |
352 | {selectedRequest && (
353 |
367 | {selectedRequest.guestName}
368 |
369 |
370 | {selectedRequest.pickupAddress}
371 |
372 |
373 |
374 | {selectedRequest.dropoffAddress}
375 |
376 |
377 |
378 |
379 |
380 | {selectedRequest.requestTime.toLocaleTimeString()}
381 |
382 |
383 |
384 |
385 | {selectedRequest.phoneNumber}
386 |
387 |
388 |
389 | )}
390 |
391 |
392 |
393 |
394 | List
395 |
396 |
397 |
398 | Map
399 |
400 |
401 | >
402 | );
403 | }
404 |
405 | const styles = StyleSheet.create({
406 | container: {
407 | flex: 1,
408 | },
409 | map: {
410 | flex: 1,
411 | },
412 | currentLocationMarker: {
413 | alignItems: 'center',
414 | justifyContent: 'center',
415 | },
416 | currentLocationDot: {
417 | width: 20,
418 | height: 20,
419 | borderRadius: 10,
420 | backgroundColor: '#4285F4',
421 | borderWidth: 3,
422 | borderColor: 'white',
423 | elevation: 2,
424 | shadowColor: '#000',
425 | shadowOffset: { width: 0, height: 1 },
426 | shadowOpacity: 0.2,
427 | shadowRadius: 2,
428 | },
429 | accuracyCircle: {
430 | position: 'absolute',
431 | backgroundColor: 'rgba(66, 133, 244, 0.2)',
432 | borderWidth: 1,
433 | borderColor: 'rgba(66, 133, 244, 0.3)',
434 | },
435 | markerContainer: {
436 | alignItems: 'center',
437 | justifyContent: 'center',
438 | },
439 | markerBackground: {
440 | backgroundColor: '#ff6d00',
441 | width: 40,
442 | height: 40,
443 | borderRadius: 20,
444 | alignItems: 'center',
445 | justifyContent: 'center',
446 | elevation: 3,
447 | shadowColor: '#000',
448 | shadowOffset: { width: 0, height: 2 },
449 | shadowOpacity: 0.25,
450 | shadowRadius: 4,
451 | },
452 | requestInfo: {
453 | position: 'absolute',
454 | bottom: 80,
455 | left: 16,
456 | right: 16,
457 | backgroundColor: 'white',
458 | borderRadius: 16,
459 | padding: 16,
460 | elevation: 4,
461 | shadowColor: '#000',
462 | shadowOffset: { width: 0, height: 2 },
463 | shadowOpacity: 0.25,
464 | shadowRadius: 4,
465 | },
466 | errorContainer: {
467 | position: 'absolute',
468 | top: 16,
469 | left: 16,
470 | right: 16,
471 | backgroundColor: '#ffebee',
472 | borderRadius: 12,
473 | padding: 12,
474 | elevation: 3,
475 | },
476 | errorText: {
477 | color: '#c62828',
478 | textAlign: 'center',
479 | fontWeight: '500',
480 | },
481 | guestName: {
482 | fontSize: 20,
483 | fontWeight: 'bold',
484 | marginBottom: 8,
485 | color: '#1a237e',
486 | },
487 | addressContainer: {
488 | flexDirection: 'row',
489 | alignItems: 'center',
490 | marginBottom: 8,
491 | gap: 8,
492 | },
493 | address: {
494 | flex: 1,
495 | fontSize: 14,
496 | color: '#333',
497 | },
498 | infoRow: {
499 | flexDirection: 'row',
500 | justifyContent: 'space-between',
501 | marginTop: 8,
502 | },
503 | infoItem: {
504 | flexDirection: 'row',
505 | alignItems: 'center',
506 | gap: 4,
507 | },
508 | infoText: {
509 | fontSize: 14,
510 | color: '#666',
511 | },
512 | tabBar: {
513 | flexDirection: 'row',
514 | backgroundColor: 'white',
515 | borderTopWidth: 1,
516 | borderTopColor: '#e0e0e0',
517 | elevation: 4,
518 | shadowColor: '#000',
519 | shadowOffset: { width: 0, height: -2 },
520 | shadowOpacity: 0.1,
521 | shadowRadius: 4,
522 | },
523 | tab: {
524 | flex: 1,
525 | paddingVertical: 16,
526 | alignItems: 'center',
527 | borderRadius: 8,
528 | margin: 4,
529 | },
530 | activeTab: {
531 | backgroundColor: '#4CAF50',
532 | },
533 | tabText: {
534 | fontSize: 16,
535 | color: '#666',
536 | fontWeight: '500',
537 | },
538 | activeTabText: {
539 | fontSize: 16,
540 | color: '#fff',
541 | fontWeight: 'bold',
542 | },
543 | });
544 |
--------------------------------------------------------------------------------