import 'react-native-get-random-values'; // ✅ must be first
import React from 'react';
import { StyleSheet, StatusBar } from 'react-native';
import MapScreen from './components/MapScreen';
import {SafeAreaView} from 'react-native-safe-area-context'
export default function App() {
return (
<SafeAreaView style={styles.container}>
<StatusBar barStyle="dark-content" />
<MapScreen />
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#f2f6fb' },
});
//package.json
{
"name": "test",
"version": "0.0.1",
"private": true,
"scripts": {
"android": "react-native run-android",
"ios": "react-native run-ios",
"lint": "eslint .",
"start": "react-native start",
"test": "jest"
},
"dependencies": {
"@react-native-community/geolocation": "^3.4.0",
"@react-native/new-app-screen": "0.82.1",
"react": "19.1.1",
"react-native": "0.82.1",
"react-native-get-random-values": "^2.0.0",
"react-native-maps": "^1.26.18",
"react-native-safe-area-context": "^5.5.2",
"react-native-vector-icons": "^10.3.0"
},
"devDependencies": {
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.3",
"@babel/runtime": "^7.25.0",
"@react-native-community/cli": "20.0.0",
"@react-native-community/cli-platform-android": "20.0.0",
"@react-native-community/cli-platform-ios": "20.0.0",
"@react-native/babel-preset": "0.82.1",
"@react-native/eslint-config": "0.82.1",
"@react-native/metro-config": "0.82.1",
"@react-native/typescript-config": "0.82.1",
"@types/jest": "^29.5.13",
"@types/react": "^19.1.1",
"@types/react-test-renderer": "^19.1.0",
"eslint": "^8.19.0",
"jest": "^29.6.3",
"prettier": "2.8.8",
"react-test-renderer": "19.1.1",
"typescript": "^5.8.3"
},
"engines": {
"node": ">=20"
}
}
//// components/MapScreen.js
import React, { useEffect, useRef, useState } from 'react';
import {
View,
StyleSheet,
Dimensions,
Animated,
Platform,
PermissionsAndroid,
TouchableOpacity,
Text,
Alert,
TextInput,
ActivityIndicator,
Keyboard,
} from 'react-native';
import MapView, { Marker, Circle, PROVIDER_GOOGLE, Polyline } from 'react-native-maps';
import Geolocation from '@react-native-community/geolocation';
import Icon from 'react-native-vector-icons/Ionicons';
import RouteCard from './RouteCard';
import FabGroup from './FabGroup';
import SearchBar from './SearchBar';
const { width, height } = Dimensions.get('window');
// ---------- Helpers ----------
function haversineDistance(a, b) {
const R = 6371000;
const toRad = (deg) => (deg * Math.PI) / 180;
const dLat = toRad(b.latitude - a.latitude);
const dLon = toRad(b.longitude - a.longitude);
const lat1 = toRad(a.latitude);
const lat2 = toRad(b.latitude);
const sinHalfLat = Math.sin(dLat / 2);
const sinHalfLon = Math.sin(dLon / 2);
const aa = sinHalfLat * sinHalfLat + Math.cos(lat1) * Math.cos(lat2) * sinHalfLon * sinHalfLon;
const c = 2 * Math.atan2(Math.sqrt(aa), Math.sqrt(1 - aa));
return R * c;
}
function decodePolyline(encoded) {
let points = [];
let index = 0, len = encoded.length;
let lat = 0, lng = 0;
while (index < len) {
let b, shift = 0, result = 0;
do {
b = encoded.charCodeAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
const dlat = ((result & 1) ? ~(result >> 1) : (result >> 1));
lat += dlat;
shift = 0;
result = 0;
do {
b = encoded.charCodeAt(index++) - 63;
result |= (b & 0x1f) << shift;
shift += 5;
} while (b >= 0x20);
const dlng = ((result & 1) ? ~(result >> 1) : (result >> 1));
lng += dlng;
points.push({ latitude: lat / 1e5, longitude: lng / 1e5 });
}
return points;
}
function sumDistancesAlongPath(points, startIndex = 0) {
let sum = 0;
for (let i = startIndex; i < points.length - 1; i++) {
sum += haversineDistance(points[i], points[i + 1]);
}
return sum;
}
function findNearestPointIndex(points, loc) {
let minD = Infinity;
let idx = 0;
for (let i = 0; i < points.length; i++) {
const d = haversineDistance(points[i], loc);
if (d < minD) {
minD = d;
idx = i;
}
}
return idx;
}
// ---------- Component ----------
export default function MapScreen() {
const [coords, setCoords] = useState(null);
const [accuracy, setAccuracy] = useState(null);
const [following, setFollowing] = useState(true);
// start / dest points
const [startPoint, setStartPoint] = useState(null);
const [destination, setDestination] = useState(null);
// route state (polyline + info)
const [routeCoords, setRouteCoords] = useState([]);
const [routeInfo, setRouteInfo] = useState(null);
const [remainingMeters, setRemainingMeters] = useState(null);
// panel + inputs
const [panelOpen, setPanelOpen] = useState(false);
const [startQuery, setStartQuery] = useState('');
const [destQuery, setDestQuery] = useState('');
const [loadingStart, setLoadingStart] = useState(false);
const [loadingDest, setLoadingDest] = useState(false);
// mode + signals
const [mode, setMode] = useState('driving'); // driving | walking
const [clearSignalCounter, setClearSignalCounter] = useState(0);
// indicates whether user pressed Go to start navigation
const [routeStarted, setRouteStarted] = useState(false);
const mapRef = useRef(null);
const markerAnim = useRef(new Animated.Value(0)).current;
const watchIdRef = useRef(null);
const GOOGLE_API_KEY = 'AIzaSyA1o2H3l50BEe0J3HtKMU7kVpyhb40j5oE'; // move to env in production
// watch device location
useEffect(() => {
async function requestPermission() {
if (Platform.OS === 'android') {
try {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION,
{
title: 'Location permission',
message: 'App needs access to your location for navigation',
buttonNeutral: 'Ask Me Later',
buttonNegative: 'Cancel',
buttonPositive: 'OK',
}
);
if (granted !== PermissionsAndroid.RESULTS.GRANTED) {
Alert.alert('Location permission denied', 'App will not be able to get your location.');
}
} catch (err) {
console.warn(err);
}
}
}
requestPermission();
watchIdRef.current = Geolocation.watchPosition(
(pos) => {
const c = pos.coords;
const newCoords = { latitude: c.latitude, longitude: c.longitude, heading: c.heading || 0 };
setCoords(newCoords);
setAccuracy(c.accuracy);
Animated.sequence([
Animated.timing(markerAnim, { toValue: 1, duration: 150, useNativeDriver: true }),
Animated.timing(markerAnim, { toValue: 0, duration: 150, useNativeDriver: true })
]).start();
if (following && mapRef.current) {
try {
mapRef.current.animateToRegion(
{ latitude: c.latitude, longitude: c.longitude, latitudeDelta: 0.01, longitudeDelta: 0.01 },
400
);
} catch (e) {}
}
if (routeCoords && routeCoords.length > 0) {
const nearestIdx = findNearestPointIndex(routeCoords, newCoords);
const remainAlongPath = sumDistancesAlongPath(routeCoords, nearestIdx);
setRemainingMeters(Math.round(remainAlongPath));
}
},
(err) => { console.warn(err); },
{ enableHighAccuracy: true, distanceFilter: 1, interval: 2000, fastestInterval: 1000 }
);
return () => {
if (watchIdRef.current != null) Geolocation.clearWatch(watchIdRef.current);
};
}, [following, routeCoords]);
const recenter = () => {
if (coords && mapRef.current) {
mapRef.current.animateToRegion(
{ latitude: coords.latitude, longitude: coords.longitude, latitudeDelta: 0.01, longitudeDelta: 0.01 },
400
);
setFollowing(true);
}
};
// Nominatim geocode helper (single-result)
async function geocodeQuery(q) {
if (!q || !q.trim()) return null;
const url = `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(q.trim())}&limit=1`;
try {
const res = await fetch(url, {
headers: {
'User-Agent': 'my-geolocation-app/1.0 (contact: myemail@example.com)',
'Accept-Language': 'en',
},
});
const text = await res.text();
const data = JSON.parse(text || '[]');
if (data && data.length > 0) {
const { lat, lon, display_name } = data[0];
const latN = parseFloat(lat), lonN = parseFloat(lon);
if (!isNaN(latN) && !isNaN(lonN)) {
return { latitude: latN, longitude: lonN, label: display_name };
}
}
return null;
} catch (e) {
console.warn('geocode error', e);
return null;
}
}
// panel search handlers (start / dest fields)
const onSearchStart = async () => {
if (!startQuery.trim()) return;
Keyboard.dismiss();
setLoadingStart(true);
const res = await geocodeQuery(startQuery);
setLoadingStart(false);
if (res) {
setStartPoint(res);
if (mapRef.current) mapRef.current.animateToRegion({ latitude: res.latitude, longitude: res.longitude, latitudeDelta: 0.02, longitudeDelta: 0.02 }, 400);
// if dest already exists and routeStarted true or both points present -> auto-fetch if user expects immediate route
if (destination && routeStarted) await fetchRoute(res, destination, mode);
} else {
Alert.alert('No results', 'Could not find a location for the Start query.');
}
};
const onSearchDest = async () => {
if (!destQuery.trim()) return;
Keyboard.dismiss();
setLoadingDest(true);
const res = await geocodeQuery(destQuery);
setLoadingDest(false);
if (res) {
setDestination(res);
if (mapRef.current) mapRef.current.animateToRegion({ latitude: res.latitude, longitude: res.longitude, latitudeDelta: 0.02, longitudeDelta: 0.02 }, 400);
// if user already pressed Go (routeStarted) and both points available -> fetch
if (startPoint && routeStarted) {
await fetchRoute(startPoint, res, mode);
} else if (!startPoint && coords && routeStarted) {
// origin is current location
await fetchRoute(coords, res, mode);
}
} else {
Alert.alert('No results', 'Could not find a location for the Destination query.');
}
};
// fetch route via Google Directions API
const fetchRoute = async (origin, dest, travelMode = 'driving') => {
try {
if (!origin || !dest) return;
const originParam = `${origin.latitude},${origin.longitude}`;
const destParam = `${dest.latitude},${dest.longitude}`;
const url = `https://maps.googleapis.com/maps/api/directions/json?origin=${originParam}&destination=${destParam}&mode=${travelMode}&key=${GOOGLE_API_KEY}`;
const res = await fetch(url);
const json = await res.json();
if (json.status !== 'OK') {
console.warn('Directions error', json);
Alert.alert('Route error', `Directions API: ${json.status}`);
setRouteCoords([]);
setRouteInfo(null);
return;
}
const r = json.routes[0];
const points = decodePolyline(r.overview_polyline.points);
setRouteCoords(points);
let distanceMeters = 0, durationSec = 0;
if (r.legs && r.legs.length > 0) {
r.legs.forEach((leg) => {
if (leg.distance && leg.distance.value) distanceMeters += leg.distance.value;
if (leg.duration && leg.duration.value) durationSec += leg.duration.value;
});
}
setRouteInfo({
distanceMeters,
durationSec,
distanceText: r.legs && r.legs.length > 0 ? r.legs[0].distance.text : `${(distanceMeters/1000).toFixed(1)} km`,
durationText: r.legs && r.legs.length > 0 ? r.legs[0].duration.text : `${Math.round(durationSec/60)} min`
});
const nearestIdx = findNearestPointIndex(points, origin);
const remainAlongPath = sumDistancesAlongPath(points, nearestIdx);
setRemainingMeters(Math.round(remainAlongPath));
} catch (e) {
console.warn('fetchRoute error', e);
Alert.alert('Route error', e.message || 'Could not fetch route');
setRouteCoords([]);
setRouteInfo(null);
}
};
// Go pressed (inside RouteCard)
const onGoPressed = async () => {
if (!destination) {
Alert.alert('No destination', 'Please set a destination first.');
return;
}
const origin = startPoint || coords;
if (!origin) {
Alert.alert('No start', 'Current location not available yet.');
return;
}
// mark that route has started (this will reveal mode icons etc.)
setRouteStarted(true);
await fetchRoute(origin, destination, mode);
};
// clear everything
const clearMap = () => {
setStartPoint(null);
setDestination(null);
setRouteCoords([]);
setRouteInfo(null);
setRemainingMeters(null);
setPanelOpen(false);
setStartQuery('');
setDestQuery('');
setMode('driving');
setFollowing(true);
setRouteStarted(false);
setClearSignalCounter((c) => c + 1);
if (coords && mapRef.current) {
mapRef.current.animateToRegion({ latitude: coords.latitude, longitude: coords.longitude, latitudeDelta: 0.01, longitudeDelta: 0.01 }, 300);
}
};
// mode change (only visible after routeStarted)
const changeMode = async (newMode) => {
setMode(newMode);
if (!routeStarted) return;
// if route already started and both points exist, refetch
const origin = startPoint || coords;
if (origin && destination) {
await fetchRoute(origin, destination, newMode);
}
};
// compute straight-line distance to show before route is fetched
const computeStraightDistanceText = () => {
const origin = startPoint || coords;
if (!origin || !destination) return '-';
const meters = Math.round(haversineDistance(origin, destination));
if (meters >= 1000) return `${(meters / 1000).toFixed(1)} km`;
return `${meters} m`;
};
const formatDistance = (m) => {
if (m == null) return '-';
if (m >= 1000) return `${(m / 1000).toFixed(1)} km`;
return `${Math.round(m)} m`;
};
const formatDuration = (sec) => {
if (!sec && sec !== 0) return '-';
const mins = Math.round(sec / 60);
if (mins >= 60) {
const h = Math.floor(mins / 60);
const rem = mins % 60;
return `${h}h ${rem}m`;
}
return `${mins} min`;
};
// prepare props for RouteCard
const routeCardProps = {
destinationLabel: destination ? destination.label : null,
// before route started show straight-line distance; after started show actual route distance
totalDistanceText: routeStarted && routeInfo ? routeInfo.distanceText : computeStraightDistanceText(),
totalDurationText: routeStarted && routeInfo ? routeInfo.durationText : '-',
remainingDistanceText: formatDistance(remainingMeters),
mode,
showDetails: routeStarted && !!routeInfo, // show full details only after route started AND routeInfo present
onGoPress: onGoPressed,
};
return (
<View style={styles.container}>
{/* keep original SearchBar at top */}
<SearchBar onPlaceSelected={({ latitude, longitude, label }) => {
const dest = { latitude, longitude, label: label || 'Destination' };
setDestination(dest);
setRouteCoords([]);
setRouteInfo(null);
setRemainingMeters(null);
setFollowing(false);
// center on destination
if (mapRef.current) mapRef.current.animateToRegion({ latitude, longitude, latitudeDelta: 0.02, longitudeDelta: 0.02 }, 500);
// only auto-route if a custom startPoint exists and routeStarted is true
if (startPoint && routeStarted) {
fetchRoute(startPoint, dest, mode);
}
}} clearSignal={clearSignalCounter} />
<MapView
ref={mapRef}
style={styles.map}
provider={Platform.OS === 'android' ? PROVIDER_GOOGLE : null}
showsUserLocation={false}
showsMyLocationButton={false}
loadingEnabled={true}
initialRegion={{ latitude: 20.5937, longitude: 78.9629, latitudeDelta: 20, longitudeDelta: 20 }}
>
{coords && (
<>
<Marker coordinate={{ latitude: coords.latitude, longitude: coords.longitude }}>
<Animated.View style={[styles.blueDot, { transform: [{ scale: markerAnim.interpolate({ inputRange: [0, 1], outputRange: [1, 1.35] }) }] }]} />
</Marker>
<Circle center={{ latitude: coords.latitude, longitude: coords.longitude }} radius={accuracy || 20}
strokeColor={'rgba(0,122,255,0.6)'} fillColor={'rgba(0,122,255,0.15)'} />
</>
)}
{startPoint && (
<Marker coordinate={{ latitude: startPoint.latitude, longitude: startPoint.longitude }} pinColor="green" title={startPoint.label} />
)}
{destination && (
<Marker coordinate={{ latitude: destination.latitude, longitude: destination.longitude }} pinColor="red" title={destination.label} />
)}
{routeCoords && routeCoords.length > 0 && (
<Polyline
key={mode}
coordinates={routeCoords}
strokeWidth={5}
strokeColor={'rgba(0,122,255,0.9)'}
lineCap="round"
lineJoin="round"
lineDashPattern={mode === 'walking' ? [10, 6] : undefined}
/>
)}
</MapView>
{/* Arrow icon - moved to RIGHT side above other bottom icons */}
<TouchableOpacity style={styles.arrowIconRight} onPress={() => setPanelOpen(s => !s)}>
<Icon name="swap-horizontal-outline" size={26} color="#111" />
</TouchableOpacity>
{/* Mode icons - visible only after Go (routeStarted) */}
{routeStarted && (
<View style={styles.modeRow}>
<TouchableOpacity style={[styles.iconBtn, mode === 'driving' ? styles.iconActive : null]} onPress={() => changeMode('driving')}>
<Icon name="car-outline" size={20} color={mode === 'driving' ? '#fff' : '#007AFF'} />
</TouchableOpacity>
<TouchableOpacity style={[styles.iconBtn, mode === 'walking' ? styles.iconActive : null]} onPress={() => changeMode('walking')}>
<Icon name="walk-outline" size={20} color={mode === 'walking' ? '#fff' : '#007AFF'} />
</TouchableOpacity>
</View>
)}
{/* Top panel with Start / Destination inputs (opens from right arrow) */}
{panelOpen && (
<View style={styles.panel}>
<View style={styles.inputRow}>
<TextInput
style={styles.input}
placeholder="Start (type address or place)..."
value={startQuery}
onChangeText={setStartQuery}
returnKeyType="search"
onSubmitEditing={onSearchStart}
/>
<TouchableOpacity style={styles.iconBtnSmall} onPress={onSearchStart}>
{loadingStart ? <ActivityIndicator size="small" /> : <Icon name="search" size={18} color="#007AFF" />}
</TouchableOpacity>
<TouchableOpacity style={styles.iconBtnSmall} onPress={() => {
if (coords) {
setStartPoint({ latitude: coords.latitude, longitude: coords.longitude, label: 'Current location' });
setStartQuery('Current location');
Keyboard.dismiss();
if (destination && routeStarted) fetchRoute({ latitude: coords.latitude, longitude: coords.longitude }, destination, mode);
} else {
Alert.alert('No current location', 'Waiting for device location.');
}
}}>
<Icon name="locate-outline" size={20} color="#007AFF" />
</TouchableOpacity>
</View>
<View style={styles.inputRow}>
<TextInput
style={styles.input}
placeholder="Destination (type address or place)..."
value={destQuery}
onChangeText={setDestQuery}
returnKeyType="search"
onSubmitEditing={onSearchDest}
/>
<TouchableOpacity style={styles.iconBtnSmall} onPress={onSearchDest}>
{loadingDest ? <ActivityIndicator size="small" /> : <Icon name="search" size={18} color="#007AFF" />}
</TouchableOpacity>
</View>
</View>
)}
{/* RouteCard visible when destination exists (shows straight distance until Go pressed) */}
{destination && (
<View style={styles.routeCardWrap}>
<RouteCard
destinationLabel={routeCardProps.destinationLabel}
totalDistanceText={routeCardProps.totalDistanceText}
totalDurationText={routeCardProps.totalDurationText}
remainingDistanceText={routeCardProps.remainingDistanceText}
mode={routeCardProps.mode}
showDetails={routeCardProps.showDetails}
onGoPress={routeCardProps.onGoPress}
/>
</View>
)}
{/* Clear button at bottom (red) */}
<TouchableOpacity style={styles.clearBottomBtn} onPress={clearMap}>
<Text style={styles.clearBottomText}>Clear</Text>
</TouchableOpacity>
<FabGroup following={following} onToggleFollow={() => setFollowing((v) => !v)} onRecenter={recenter} />
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
map: { width, height },
blueDot: {
width: 18, height: 18, borderRadius: 9, backgroundColor: '#007AFF', borderWidth: 3, borderColor: 'white',
shadowColor: '#007AFF', shadowOpacity: 0.6, shadowRadius: 6, elevation: 4
},
arrowIconRight: {
position: 'absolute',
right: 20,
bottom: 280, // above other bottom icons
zIndex: 95,
backgroundColor: 'orange',
padding: 8,
borderRadius: 15,
shadowColor: '#000',
shadowOpacity: 0.12,
shadowRadius: 4,
elevation: 6
},
modeRow: {
position: 'absolute', right: 16, bottom: 500, zIndex: 90, flexDirection: 'column', alignItems: 'center',
},
iconBtn: {
backgroundColor: 'white', padding: 10, borderRadius: 26, marginBottom: 8, shadowColor: '#000', shadowOpacity: 0.12,
shadowRadius: 4, elevation: 6, minWidth: 46, alignItems: 'center', justifyContent: 'center'
},
iconActive: { backgroundColor: '#007AFF' },
panel: {
position: 'absolute', left: 12, right: 12, top: 10, zIndex: 95, backgroundColor: 'white', borderRadius: 10,
padding: 8, elevation: 8, shadowColor: '#000', shadowOpacity: 0.12, shadowRadius: 6
},
inputRow: { flexDirection: 'row', alignItems: 'center', marginVertical: 6 },
input: { flex: 1, height: 44, paddingHorizontal: 10, borderRadius: 6, borderWidth: 1, borderColor: '#eee' },
iconBtnSmall: { paddingHorizontal: 8 },
goFab: {
position: 'absolute', right: 16, bottom: 140, zIndex: 80, width: 64, height: 64, borderRadius: 32, backgroundColor: '#007AFF',
alignItems: 'center', justifyContent: 'center', elevation: 8, shadowColor: '#000', shadowOpacity: 0.18, shadowRadius: 6
},
routeCardWrap: {
position: 'absolute', left: 16, right: 16, bottom:0, zIndex: 80,
},
clearBottomBtn: {
position: 'absolute',
right: 16,
bottom: 10,
zIndex: 95,
backgroundColor: '#D9534F',
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 999,
elevation: 8,
shadowColor: '#000',
shadowOpacity: 0.2,
shadowRadius: 6,
},
clearBottomText: { color: '#fff', fontWeight: '700' },
});
//FabGroup.js
import React from 'react';
import { View, TouchableOpacity, Text, StyleSheet } from 'react-native';
export default function FabGroup({ following, onToggleFollow, onRecenter }) {
return (
<View style={styles.container} pointerEvents="box-none">
<TouchableOpacity style={styles.fab} onPress={onRecenter}>
<Text style={styles.icon}>⤴︎</Text>
</TouchableOpacity>
<TouchableOpacity style={[styles.fab, following ? styles.active : null]} onPress={onToggleFollow}>
<Text style={styles.icon}>{following ? '🔒' : '🔓'}</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: { position: 'absolute', right: 16, bottom: 20, zIndex: 30, alignItems: 'center' },
fab: {
bottom:300,
width: 52,
height: 52,
borderRadius: 26,
backgroundColor: 'white',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 12,
shadowColor: '#000',
shadowOpacity: 0.16,
shadowRadius: 6,
elevation: 8
},
active: { backgroundColor: '#007AFF' },
icon: { fontSize: 20 }
});
//
// components/RouteCard.js
import React from 'react';
import { View, Text, StyleSheet, TouchableOpacity } from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
export default function RouteCard({
destinationLabel,
totalDistanceText,
totalDurationText,
remainingDistanceText,
mode,
showDetails, // when true show full details; when false show only destination + straight distance
onGoPress, // callback when Go is pressed
}) {
return (
<View style={styles.card}>
<View style={styles.headerRow}>
<Text style={styles.title} numberOfLines={1}>
{destinationLabel || 'Destination'}
</Text>
{/* Go button sits inside the card */}
<TouchableOpacity style={styles.goBtn} onPress={onGoPress}>
<Icon name="navigate-circle" size={30} color="#fff" />
</TouchableOpacity>
</View>
{/* If showDetails is false, show only the straight-line distance (totalDistanceText) */}
{!showDetails ? (
<Text style={styles.row}>
<Text style={styles.label}>Distance:</Text> {totalDistanceText || '-'}
</Text>
) : (
// Full details
<>
<Text style={styles.row}>
<Text style={styles.label}>Mode:</Text> {mode}
</Text>
<Text style={styles.row}>
<Text style={styles.label}>Route:</Text> {totalDistanceText} • {totalDurationText}
</Text>
<Text style={styles.row}>
<Text style={styles.label}>Remaining:</Text> {remainingDistanceText}
</Text>
</>
)}
</View>
);
}
const styles = StyleSheet.create({
card: {
position: 'absolute',
left: 12,
right: 12,
bottom: 80,
backgroundColor: 'white',
borderRadius: 10,
padding: 12,
minWidth: 200,
shadowColor: '#000',
shadowOpacity: 0.12,
shadowRadius: 8,
elevation: 6,
flexDirection: 'column'
},
headerRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between'
},
title: { fontWeight: '700', fontSize: 14, marginBottom: 6, flex: 1, marginRight: 8 },
row: { fontSize: 13, color: '#333', marginTop: 4 },
label: { fontWeight: '600', color: '#444' },
goBtn: {
width: 44,
height: 44,
borderRadius: 22,
backgroundColor: '#007AFF',
alignItems: 'center',
justifyContent: 'center',
},
});
//
// components/SearchBar.js
import React, { useState, useEffect } from 'react';
import {
View,
TextInput,
StyleSheet,
TouchableOpacity,
ActivityIndicator,
Alert,
Keyboard,
} from 'react-native';
import Icon from 'react-native-vector-icons/Ionicons';
export default function SearchBar({ onPlaceSelected, clearSignal }) {
const [query, setQuery] = useState('');
const [loading, setLoading] = useState(false);
// 🔁 When clearSignal changes, reset input box
useEffect(() => {
if (clearSignal) {
setQuery('');
Keyboard.dismiss();
}
}, [clearSignal]);
const handleSearch = async () => {
if (!query.trim()) return;
Keyboard.dismiss(); // ✅ close keyboard when searching
setLoading(true);
try {
const url = `https://nominatim.openstreetmap.org/search?format=json&q=${encodeURIComponent(
query.trim()
)}&limit=5`;
const res = await fetch(url, {
headers: {
'User-Agent': 'my-geolocation-app/1.0 (contact: myemail@example.com)',
'Accept-Language': 'en',
},
});
const text = await res.text();
let data = JSON.parse(text || '[]');
if (data && data.length > 0) {
// Use the first result
const { lat, lon, display_name } = data[0];
const latitude = parseFloat(lat);
const longitude = parseFloat(lon);
if (!isNaN(latitude) && !isNaN(longitude)) {
onPlaceSelected({ latitude, longitude, label: display_name });
} else {
Alert.alert('Invalid location returned.');
}
} else {
Alert.alert('No results', 'Could not find any location for your search.');
}
} catch (e) {
console.warn('Search error:', e);
Alert.alert('Search error', e.message || 'Unknown error');
} finally {
setLoading(false);
}
};
return (
<View style={styles.container}>
<View style={styles.inputContainer}>
<TextInput
placeholder="Enter destination (city, place...)"
value={query}
onChangeText={setQuery}
style={styles.input}
returnKeyType="search"
onSubmitEditing={handleSearch}
blurOnSubmit={true} // ✅ auto dismiss on keyboard search press
/>
<TouchableOpacity onPress={handleSearch} style={styles.iconButton}>
{loading ? (
<ActivityIndicator size="small" color="#007AFF" />
) : (
<Icon name="search" size={22} color="#007AFF" />
)}
</TouchableOpacity>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: { position: 'absolute', top: 10, left: 12, right: 12, zIndex: 40 },
inputContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'white',
borderRadius: 10,
elevation: 6,
shadowColor: '#000',
shadowOpacity: 0.1,
shadowRadius: 4,
},
input: {
flex: 1,
height: 44,
paddingHorizontal: 12,
color: '#222',
fontSize: 16,
},
iconButton: {
paddingHorizontal: 12,
},
});
//StatusCard.js
import React from 'react';
import { View, Text, StyleSheet } from 'react-native';
export default function StatusCard({ coords, accuracy }) {
return (
<View style={styles.card} pointerEvents="none">
<Text style={styles.row}>
<Text style={styles.label}>Lat:</Text> {coords ? coords.latitude.toFixed(6) : '-'}
</Text>
<Text style={styles.row}>
<Text style={styles.label}>Lng:</Text> {coords ? coords.longitude.toFixed(6) : '-'}
</Text>
<Text style={styles.row}>
<Text style={styles.label}>Accuracy:</Text> {accuracy ? `${Math.round(accuracy)} m` : '-'}
</Text>
</View>
);
}
const styles = StyleSheet.create({
card: {
position: 'absolute',
left: 12,
bottom: 160,
backgroundColor: 'white',
borderRadius: 10,
padding: 10,
minWidth: 180,
shadowColor: '#000',
shadowOpacity: 0.12,
shadowRadius: 8,
elevation: 6
},
row: { fontSize: 12, color: '#333' },
label: { fontWeight: '600', color: '#444' }
});
//
//app/build.gradle
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
//mefest
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application
android:name=".MainApplication"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:allowBackup="false"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="${usesCleartextTraffic}"
android:supportsRtl="true">
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyA1o2H3l50BEe0J3HtKMU7kVpyhb40j5oE" />
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize|uiMode"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>