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