diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 1a78b88..7280671 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -3,6 +3,8 @@
+
+
+
+
diff --git a/package-lock.json b/package-lock.json
index ae7b60b..3b035e4 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
"lynkeduppro-login-sdk": "^0.1.9",
"react": "19.1.1",
"react-native": "0.82.1",
+ "react-native-map-clustering": "^4.0.0",
"react-native-maps": "^1.26.20",
"react-native-permissions": "^5.4.4",
"react-native-safe-area-context": "^5.5.2",
@@ -2651,6 +2652,15 @@
"@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": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@mapbox/polyline/-/polyline-1.2.1.tgz",
@@ -2662,6 +2672,17 @@
"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": {
"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",
@@ -8379,6 +8400,12 @@
"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": {
"version": "4.5.4",
"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": {
"version": "1.26.20",
"resolved": "https://registry.npmjs.org/react-native-maps/-/react-native-maps-1.26.20.tgz",
@@ -11494,6 +11535,15 @@
],
"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": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
diff --git a/package.json b/package.json
index a636ab5..510e724 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
"lynkeduppro-login-sdk": "^0.1.9",
"react": "19.1.1",
"react-native": "0.82.1",
+ "react-native-map-clustering": "^4.0.0",
"react-native-maps": "^1.26.20",
"react-native-permissions": "^5.4.4",
"react-native-safe-area-context": "^5.5.2",
diff --git a/src/screens/Map.tsx b/src/screens/Map.tsx
index 94eefbc..9dc2355 100644
--- a/src/screens/Map.tsx
+++ b/src/screens/Map.tsx
@@ -1,60 +1,102 @@
import React, { useEffect, useRef, useState } from 'react';
-import { View, StyleSheet, PermissionsAndroid, Platform } from 'react-native';
-import MapView, { Marker, MapType, Region } from 'react-native-maps';
+import {
+ 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';
+const { height } = Dimensions.get('window');
+
+/* ---------------- CONSTANTS ---------------- */
+
const DEFAULT_REGION: Region = {
- latitude: 21.1702,
- longitude: 72.8311,
- latitudeDelta: 0.05,
- longitudeDelta: 0.05,
+ latitude: 47.6062,
+ longitude: -122.3321,
+ latitudeDelta: 0.15,
+ longitudeDelta: 0.15,
};
-const Map: React.FC = () => {
- const mapRef = useRef(null);
- const [mapType, setMapType] = useState('standard');
- const [region, setRegion] = useState(DEFAULT_REGION);
+/* ---------------- MOCK DATA ---------------- */
+
+const generateProperties = (count = 2000) =>
+ 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(() => {
- requestPermissionAndLocate();
+ const t = setTimeout(() => setTracks(false), 400);
+ return () => clearTimeout(t);
}, []);
- const requestPermissionAndLocate = async () => {
+ return (
+
+
+ {formatPrice(item.price)}
+
+
+ );
+};
+
+/* ---------------- MAIN SCREEN ---------------- */
+
+const MapScreen: React.FC = () => {
+ const mapRef = useRef(null);
+
+ const [mapType, setMapType] = useState('standard');
+ const [selectedItem, setSelectedItem] = useState(null);
+
+ /* ---- current location ---- */
+ useEffect(() => {
+ locateUser();
+ }, []);
+
+ const locateUser = async () => {
if (Platform.OS === 'android') {
const granted = await PermissionsAndroid.request(
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION
);
if (granted !== PermissionsAndroid.RESULTS.GRANTED) return;
}
- locateUser();
- };
- const locateUser = () => {
- Geolocation.getCurrentPosition(
- pos => {
- const { latitude, longitude } = pos.coords;
-
- const currentRegion: Region = {
- latitude,
- 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');
+ Geolocation.getCurrentPosition(pos => {
+ const { latitude, longitude } = pos.coords;
+ mapRef.current?.animateToRegion(
+ { latitude, longitude, latitudeDelta: 0.05, longitudeDelta: 0.05 },
+ 1000
+ );
+ });
};
return (
@@ -63,24 +105,142 @@ const Map: React.FC = () => {
ref={mapRef}
style={StyleSheet.absoluteFillObject}
initialRegion={DEFAULT_REGION}
- mapType={mapType}
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 (
+
+
+
+ {formatPrice(avgPrice)}
+
+
+
+ );
+ }}
>
-
+ {properties.map(item => (
+ setSelectedItem(item)}
+ />
+ ))}
+
+ {/* -------- BOTTOM POPUP -------- */}
+ {selectedItem && (
+
+ setSelectedItem(null)}
+ >
+ ✕
+
+
+
+ ${formatPrice(selectedItem.price)}
+
+
+
+ {selectedItem.beds} Beds • {selectedItem.baths} Baths
+
+
+
+ View Details
+
+
+ )}
);
};
+/* ---------------- STYLES ---------------- */
+
const styles = StyleSheet.create({
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;
diff --git a/yarn.lock b/yarn.lock
index a151d4e..146af32 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1344,6 +1344,13 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@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":
version "1.2.1"
resolved "https://registry.npmjs.org/@mapbox/polyline/-/polyline-1.2.1.tgz"
@@ -1351,6 +1358,11 @@
dependencies:
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":
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"
@@ -4579,6 +4591,11 @@ jsonfile@^4.0.0:
object.assign "^4.1.4"
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:
version "4.5.4"
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"
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"
resolved "https://registry.npmjs.org/react-native-maps/-/react-native-maps-1.26.20.tgz"
integrity sha512-kWibDz6wLLQ0685gOEFz5jdzm4miD7PMeVdtZV7ilgftDcusC2iy7SueBJpHF0LKCoOSa1BEUiKqpx1dBMSNpA==
@@ -6246,6 +6271,13 @@ strnum@^1.1.1:
resolved "https://registry.npmjs.org/strnum/-/strnum-1.1.2.tgz"
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:
version "7.2.0"
resolved "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz"