Add map clustering functionality and integrate Google Maps API key

This commit is contained in:
2025-12-30 23:53:08 +05:30
parent 41aac35964
commit 67bc16e194
5 changed files with 300 additions and 51 deletions

View File

@@ -3,6 +3,8 @@
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<application <application
android:name=".MainApplication" android:name=".MainApplication"
@@ -25,5 +27,9 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<meta-data
android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyAOVYRIgupAurZup5y1PRh8Ismb1A3lLao"/>
</application> </application>
</manifest> </manifest>

50
package-lock.json generated
View File

@@ -16,6 +16,7 @@
"lynkeduppro-login-sdk": "^0.1.9", "lynkeduppro-login-sdk": "^0.1.9",
"react": "19.1.1", "react": "19.1.1",
"react-native": "0.82.1", "react-native": "0.82.1",
"react-native-map-clustering": "^4.0.0",
"react-native-maps": "^1.26.20", "react-native-maps": "^1.26.20",
"react-native-permissions": "^5.4.4", "react-native-permissions": "^5.4.4",
"react-native-safe-area-context": "^5.5.2", "react-native-safe-area-context": "^5.5.2",
@@ -2651,6 +2652,15 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@mapbox/geo-viewport": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@mapbox/geo-viewport/-/geo-viewport-0.4.1.tgz",
"integrity": "sha512-5g6eM3EOSl7+0p0VY+vHWEYjUlNzof936VKHTi/NuJVABjbYe8D2NAVJ0qt5C9Np4glUlhKFepgAgQ0OEybrjQ==",
"license": "BSD-2-Clause",
"dependencies": {
"@mapbox/sphericalmercator": "~1.1.0"
}
},
"node_modules/@mapbox/polyline": { "node_modules/@mapbox/polyline": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/@mapbox/polyline/-/polyline-1.2.1.tgz", "resolved": "https://registry.npmjs.org/@mapbox/polyline/-/polyline-1.2.1.tgz",
@@ -2662,6 +2672,17 @@
"polyline": "bin/polyline.bin.js" "polyline": "bin/polyline.bin.js"
} }
}, },
"node_modules/@mapbox/sphericalmercator": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@mapbox/sphericalmercator/-/sphericalmercator-1.1.0.tgz",
"integrity": "sha512-pEsfZyG4OMThlfFQbCte4gegvHUjxXCjz0KZ4Xk8NdOYTQBLflj6U8PL05RPAiuRAMAQNUUKJuL6qYZ5Y4kAWA==",
"bin": {
"bbox": "bin/bbox.js",
"to4326": "bin/to4326.js",
"to900913": "bin/to900913.js",
"xyz": "bin/xyz.js"
}
},
"node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": {
"version": "5.1.1-v1", "version": "5.1.1-v1",
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz",
@@ -8379,6 +8400,12 @@
"node": ">=4.0" "node": ">=4.0"
} }
}, },
"node_modules/kdbush": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
"integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==",
"license": "ISC"
},
"node_modules/keyv": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -10192,6 +10219,20 @@
} }
} }
}, },
"node_modules/react-native-map-clustering": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/react-native-map-clustering/-/react-native-map-clustering-4.0.0.tgz",
"integrity": "sha512-+YNh4frhZIHQReURxYGHNy9MJ50GYWpW6psoBEjvTG6vb33eYu00GmO8Pu/9VwMB1YL5lOxZ9+sJClJ8Mz1Bxw==",
"license": "MIT",
"dependencies": {
"@mapbox/geo-viewport": "^0.4.1",
"supercluster": "^8.0.0"
},
"peerDependencies": {
"react-native": "*",
"react-native-maps": "*"
}
},
"node_modules/react-native-maps": { "node_modules/react-native-maps": {
"version": "1.26.20", "version": "1.26.20",
"resolved": "https://registry.npmjs.org/react-native-maps/-/react-native-maps-1.26.20.tgz", "resolved": "https://registry.npmjs.org/react-native-maps/-/react-native-maps-1.26.20.tgz",
@@ -11494,6 +11535,15 @@
], ],
"license": "MIT" "license": "MIT"
}, },
"node_modules/supercluster": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz",
"integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==",
"license": "ISC",
"dependencies": {
"kdbush": "^4.0.2"
}
},
"node_modules/supports-color": { "node_modules/supports-color": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",

View File

@@ -18,6 +18,7 @@
"lynkeduppro-login-sdk": "^0.1.9", "lynkeduppro-login-sdk": "^0.1.9",
"react": "19.1.1", "react": "19.1.1",
"react-native": "0.82.1", "react-native": "0.82.1",
"react-native-map-clustering": "^4.0.0",
"react-native-maps": "^1.26.20", "react-native-maps": "^1.26.20",
"react-native-permissions": "^5.4.4", "react-native-permissions": "^5.4.4",
"react-native-safe-area-context": "^5.5.2", "react-native-safe-area-context": "^5.5.2",

View File

@@ -1,60 +1,102 @@
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { View, StyleSheet, PermissionsAndroid, Platform } from 'react-native'; import {
import MapView, { Marker, MapType, Region } from 'react-native-maps'; View,
StyleSheet,
PermissionsAndroid,
Platform,
Text,
TouchableOpacity,
Dimensions,
} from 'react-native';
import MapView from 'react-native-map-clustering';
import { Marker, MapType, Region } from 'react-native-maps';
import Geolocation from '@react-native-community/geolocation'; import Geolocation from '@react-native-community/geolocation';
const { height } = Dimensions.get('window');
/* ---------------- CONSTANTS ---------------- */
const DEFAULT_REGION: Region = { const DEFAULT_REGION: Region = {
latitude: 21.1702, latitude: 47.6062,
longitude: 72.8311, longitude: -122.3321,
latitudeDelta: 0.05, latitudeDelta: 0.15,
longitudeDelta: 0.05, longitudeDelta: 0.15,
}; };
const Map: React.FC = () => { /* ---------------- MOCK DATA ---------------- */
const mapRef = useRef<MapView>(null);
const [mapType, setMapType] = useState<MapType>('standard'); const generateProperties = (count = 2000) =>
const [region, setRegion] = useState<Region>(DEFAULT_REGION); Array.from({ length: count }).map((_, i) => ({
id: `${i}`,
lat: 47.5 + Math.random() * 0.25,
lng: -122.45 + Math.random() * 0.3,
price: Math.floor(300000 + Math.random() * 4000000),
beds: Math.floor(1 + Math.random() * 5),
baths: Math.floor(1 + Math.random() * 4),
}));
const properties = generateProperties(2000);
/* ---------------- HELPERS ---------------- */
const formatPrice = (value: number) => {
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
if (value >= 1000) return `${Math.round(value / 1000)}K`;
return value.toString();
};
/* ---------------- PRICE MARKER (FIXED) ---------------- */
const PriceMarker = ({ item, onPress }: any) => {
const [tracks, setTracks] = useState(true);
useEffect(() => { useEffect(() => {
requestPermissionAndLocate(); const t = setTimeout(() => setTracks(false), 400);
return () => clearTimeout(t);
}, []); }, []);
const requestPermissionAndLocate = async () => { return (
<Marker
coordinate={{ latitude: item.lat, longitude: item.lng }}
tracksViewChanges={tracks}
onPress={onPress}
>
<View style={styles.pin}>
<Text style={styles.pinText}>{formatPrice(item.price)}</Text>
</View>
</Marker>
);
};
/* ---------------- MAIN SCREEN ---------------- */
const MapScreen: React.FC = () => {
const mapRef = useRef<MapView>(null);
const [mapType, setMapType] = useState<MapType>('standard');
const [selectedItem, setSelectedItem] = useState<any>(null);
/* ---- current location ---- */
useEffect(() => {
locateUser();
}, []);
const locateUser = async () => {
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
const granted = await PermissionsAndroid.request( const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION
); );
if (granted !== PermissionsAndroid.RESULTS.GRANTED) return; if (granted !== PermissionsAndroid.RESULTS.GRANTED) return;
} }
locateUser();
};
const locateUser = () => { Geolocation.getCurrentPosition(pos => {
Geolocation.getCurrentPosition(
pos => {
const { latitude, longitude } = pos.coords; const { latitude, longitude } = pos.coords;
mapRef.current?.animateToRegion(
const currentRegion: Region = { { latitude, longitude, latitudeDelta: 0.05, longitudeDelta: 0.05 },
latitude, 1000
longitude,
latitudeDelta: 0.01,
longitudeDelta: 0.01,
};
setRegion(currentRegion);
// ✅ Redirect to current location
mapRef.current?.animateToRegion(currentRegion, 1000);
},
err => console.log(err),
{ enableHighAccuracy: true }
); );
}; });
const onRegionChangeComplete = (r: Region) => {
setRegion(r);
setMapType(r.latitudeDelta < 0.008 ? 'satellite' : 'standard');
}; };
return ( return (
@@ -63,24 +105,142 @@ const Map: React.FC = () => {
ref={mapRef} ref={mapRef}
style={StyleSheet.absoluteFillObject} style={StyleSheet.absoluteFillObject}
initialRegion={DEFAULT_REGION} initialRegion={DEFAULT_REGION}
mapType={mapType}
showsUserLocation showsUserLocation
onRegionChangeComplete={onRegionChangeComplete} animationEnabled
> clusterPressMaxZoom={16}
spiralEnabled={false}
mapType={mapType}
onPress={() => setSelectedItem(null)}
onRegionChangeComplete={r =>
setMapType(r.latitudeDelta < 0.02 ? 'satellite' : 'standard')
}
/* ---- CLUSTER BUBBLE ---- */
renderCluster={(cluster, onPress) => {
const { geometry, properties } = cluster;
const avgPrice =
properties.point_count * 900000 / properties.point_count;
return (
<Marker <Marker
key={`cluster-${properties.cluster_id}`}
coordinate={{ coordinate={{
latitude: region.latitude, latitude: geometry.coordinates[1],
longitude: region.longitude, longitude: geometry.coordinates[0],
}} }}
title="Your Location" onPress={onPress}
>
<View style={styles.clusterBubble}>
<Text style={styles.clusterText}>
{formatPrice(avgPrice)}
</Text>
</View>
</Marker>
);
}}
>
{properties.map(item => (
<PriceMarker
key={item.id}
item={item}
onPress={() => setSelectedItem(item)}
/> />
))}
</MapView> </MapView>
{/* -------- BOTTOM POPUP -------- */}
{selectedItem && (
<View style={styles.popup}>
<TouchableOpacity
style={styles.close}
onPress={() => setSelectedItem(null)}
>
<Text></Text>
</TouchableOpacity>
<Text style={styles.price}>
${formatPrice(selectedItem.price)}
</Text>
<Text style={styles.details}>
{selectedItem.beds} Beds {selectedItem.baths} Baths
</Text>
<TouchableOpacity style={styles.btn}>
<Text style={styles.btnText}>View Details</Text>
</TouchableOpacity>
</View>
)}
</View> </View>
); );
}; };
/* ---------------- STYLES ---------------- */
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { flex: 1 }, container: { flex: 1 },
pin: {
backgroundColor: '#8B0000',
paddingHorizontal: 10,
paddingVertical: 6,
borderRadius: 18,
},
pinText: {
color: '#fff',
fontWeight: '600',
fontSize: 12,
},
clusterBubble: {
backgroundColor: '#8B0000',
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 24,
borderWidth: 2,
borderColor: '#fff',
},
clusterText: {
color: '#fff',
fontWeight: 'bold',
fontSize: 13,
},
popup: {
position: 'absolute',
bottom: 20,
left: 16,
right: 16,
backgroundColor: '#fff',
borderRadius: 16,
padding: 16,
elevation: 10,
},
close: {
position: 'absolute',
right: 12,
top: 12,
},
price: {
fontSize: 22,
fontWeight: 'bold',
},
details: {
marginVertical: 6,
color: '#666',
},
btn: {
marginTop: 12,
backgroundColor: '#8B0000',
padding: 12,
borderRadius: 10,
alignItems: 'center',
},
btnText: {
color: '#fff',
fontWeight: '600',
},
}); });
export default Map; export default MapScreen;

View File

@@ -1344,6 +1344,13 @@
"@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14" "@jridgewell/sourcemap-codec" "^1.4.14"
"@mapbox/geo-viewport@^0.4.1":
version "0.4.1"
resolved "https://registry.npmjs.org/@mapbox/geo-viewport/-/geo-viewport-0.4.1.tgz"
integrity sha512-5g6eM3EOSl7+0p0VY+vHWEYjUlNzof936VKHTi/NuJVABjbYe8D2NAVJ0qt5C9Np4glUlhKFepgAgQ0OEybrjQ==
dependencies:
"@mapbox/sphericalmercator" "~1.1.0"
"@mapbox/polyline@^1.2.1": "@mapbox/polyline@^1.2.1":
version "1.2.1" version "1.2.1"
resolved "https://registry.npmjs.org/@mapbox/polyline/-/polyline-1.2.1.tgz" resolved "https://registry.npmjs.org/@mapbox/polyline/-/polyline-1.2.1.tgz"
@@ -1351,6 +1358,11 @@
dependencies: dependencies:
meow "^9.0.0" meow "^9.0.0"
"@mapbox/sphericalmercator@~1.1.0":
version "1.1.0"
resolved "https://registry.npmjs.org/@mapbox/sphericalmercator/-/sphericalmercator-1.1.0.tgz"
integrity sha512-pEsfZyG4OMThlfFQbCte4gegvHUjxXCjz0KZ4Xk8NdOYTQBLflj6U8PL05RPAiuRAMAQNUUKJuL6qYZ5Y4kAWA==
"@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1":
version "5.1.1-v1" version "5.1.1-v1"
resolved "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz" resolved "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz"
@@ -4579,6 +4591,11 @@ jsonfile@^4.0.0:
object.assign "^4.1.4" object.assign "^4.1.4"
object.values "^1.1.6" object.values "^1.1.6"
kdbush@^4.0.2:
version "4.0.2"
resolved "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz"
integrity sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==
keyv@^4.5.3: keyv@^4.5.3:
version "4.5.4" version "4.5.4"
resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz" resolved "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz"
@@ -5520,7 +5537,15 @@ react-is@^19.1.1:
resolved "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz" resolved "https://registry.npmjs.org/react-is/-/react-is-19.2.0.tgz"
integrity sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA== integrity sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA==
react-native-maps@^1.26.20: react-native-map-clustering@^4.0.0:
version "4.0.0"
resolved "https://registry.npmjs.org/react-native-map-clustering/-/react-native-map-clustering-4.0.0.tgz"
integrity sha512-+YNh4frhZIHQReURxYGHNy9MJ50GYWpW6psoBEjvTG6vb33eYu00GmO8Pu/9VwMB1YL5lOxZ9+sJClJ8Mz1Bxw==
dependencies:
"@mapbox/geo-viewport" "^0.4.1"
supercluster "^8.0.0"
react-native-maps@*, react-native-maps@^1.26.20:
version "1.26.20" version "1.26.20"
resolved "https://registry.npmjs.org/react-native-maps/-/react-native-maps-1.26.20.tgz" resolved "https://registry.npmjs.org/react-native-maps/-/react-native-maps-1.26.20.tgz"
integrity sha512-kWibDz6wLLQ0685gOEFz5jdzm4miD7PMeVdtZV7ilgftDcusC2iy7SueBJpHF0LKCoOSa1BEUiKqpx1dBMSNpA== integrity sha512-kWibDz6wLLQ0685gOEFz5jdzm4miD7PMeVdtZV7ilgftDcusC2iy7SueBJpHF0LKCoOSa1BEUiKqpx1dBMSNpA==
@@ -6246,6 +6271,13 @@ strnum@^1.1.1:
resolved "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz" resolved "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz"
integrity sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA== integrity sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA==
supercluster@^8.0.0:
version "8.0.1"
resolved "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz"
integrity sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==
dependencies:
kdbush "^4.0.2"
supports-color@^7.1.0: supports-color@^7.1.0:
version "7.2.0" version "7.2.0"
resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz" resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz"