API integration for displaying map data
This commit is contained in:
@@ -5,65 +5,52 @@ import {
|
|||||||
PermissionsAndroid,
|
PermissionsAndroid,
|
||||||
Platform,
|
Platform,
|
||||||
Text,
|
Text,
|
||||||
TouchableOpacity,
|
|
||||||
FlatList,
|
FlatList,
|
||||||
Image,
|
Image,
|
||||||
Dimensions,
|
Dimensions,
|
||||||
Animated,
|
Animated,
|
||||||
|
ActivityIndicator,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
|
|
||||||
import MapView from 'react-native-map-clustering';
|
import MapView from 'react-native-map-clustering';
|
||||||
import { Marker, MapType, Region } from 'react-native-maps';
|
import { Marker, MapType, Region } from 'react-native-maps';
|
||||||
import Geolocation from '@react-native-community/geolocation';
|
import Geolocation from '@react-native-community/geolocation';
|
||||||
|
|
||||||
const { width } = Dimensions.get('window');
|
const { width } = Dimensions.get('window');
|
||||||
|
|
||||||
/* ---------------- CONSTANTS ---------------- */
|
/* ---------------- DEFAULT REGION (MUST EXIST) ---------------- */
|
||||||
|
|
||||||
const DEFAULT_REGION: Region = {
|
const DEFAULT_REGION: Region = {
|
||||||
latitude: 47.6062,
|
latitude: 25.48131,
|
||||||
longitude: -122.3321,
|
longitude: 81.854249,
|
||||||
latitudeDelta: 0.15,
|
latitudeDelta: 0.02,
|
||||||
longitudeDelta: 0.15,
|
longitudeDelta: 0.02,
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ---------------- MOCK DATA ---------------- */
|
|
||||||
const generateProperties = (count = 150) =>
|
|
||||||
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),
|
|
||||||
image: `https://picsum.photos/300/200?random=${i}`,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const properties = generateProperties();
|
|
||||||
|
|
||||||
/* ---------------- HELPERS ---------------- */
|
/* ---------------- HELPERS ---------------- */
|
||||||
|
|
||||||
const formatPrice = (value: number) => {
|
const formatPrice = (value: number) => {
|
||||||
|
if (!value) return '';
|
||||||
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
|
if (value >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`;
|
||||||
if (value >= 1000) return `${Math.round(value / 1000)}K`;
|
if (value >= 1000) return `${Math.round(value / 1000)}K`;
|
||||||
return value.toString();
|
return value.toString();
|
||||||
};
|
};
|
||||||
|
|
||||||
/* ---------------- PRICE MARKER ---------------- */
|
/* ---------------- PRICE MARKER ---------------- */
|
||||||
const PriceMarker = ({ item, onPress, selected }: any) => {
|
|
||||||
const [tracks, setTracks] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const PriceMarker = ({ item, onPress, selected }: any) => {
|
||||||
const t = setTimeout(() => setTracks(false), 300);
|
if (!item?.location) return null;
|
||||||
return () => clearTimeout(t);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Marker
|
<Marker
|
||||||
coordinate={{ latitude: item.lat, longitude: item.lng }}
|
coordinate={{
|
||||||
tracksViewChanges={tracks}
|
latitude: item.location.lat,
|
||||||
|
longitude: item.location.lng,
|
||||||
|
}}
|
||||||
onPress={(e) => {
|
onPress={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onPress(item);
|
onPress(item);
|
||||||
}}
|
}}
|
||||||
pinColor={selected ? 'gold' : '#8B0000'}
|
|
||||||
>
|
>
|
||||||
<View style={[styles.pin, selected && styles.selectedPin]}>
|
<View style={[styles.pin, selected && styles.selectedPin]}>
|
||||||
<Text style={styles.pinText}>{formatPrice(item.price)}</Text>
|
<Text style={styles.pinText}>{formatPrice(item.price)}</Text>
|
||||||
@@ -73,25 +60,36 @@ const PriceMarker = ({ item, onPress, selected }: any) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/* ---------------- MAIN SCREEN ---------------- */
|
/* ---------------- MAIN SCREEN ---------------- */
|
||||||
|
|
||||||
const MapScreen: React.FC = () => {
|
const MapScreen: React.FC = () => {
|
||||||
const mapRef = useRef<MapView>(null);
|
const mapRef = useRef<MapView>(null);
|
||||||
|
|
||||||
|
const [region, setRegion] = useState<Region>(DEFAULT_REGION);
|
||||||
const [mapType, setMapType] = useState<MapType>('standard');
|
const [mapType, setMapType] = useState<MapType>('standard');
|
||||||
|
const [properties, setProperties] = useState<any[]>([]);
|
||||||
const [selectedItem, setSelectedItem] = useState<any>(null);
|
const [selectedItem, setSelectedItem] = useState<any>(null);
|
||||||
const [popupAnim] = useState(new Animated.Value(0));
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
const popupAnim = useRef(new Animated.Value(0)).current;
|
||||||
|
|
||||||
|
/* ---------------- LIFE CYCLE ---------------- */
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
locateUser();
|
requestLocation();
|
||||||
|
fetchProperties();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Animated.timing(popupAnim, {
|
Animated.timing(popupAnim, {
|
||||||
toValue: selectedItem ? 1 : 0,
|
toValue: selectedItem ? 1 : 0,
|
||||||
duration: 300,
|
duration: 250,
|
||||||
useNativeDriver: true,
|
useNativeDriver: true,
|
||||||
}).start();
|
}).start();
|
||||||
}, [selectedItem]);
|
}, [selectedItem]);
|
||||||
|
|
||||||
const locateUser = async () => {
|
/* ---------------- LOCATION ---------------- */
|
||||||
|
|
||||||
|
const requestLocation = 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
|
||||||
@@ -101,64 +99,101 @@ const MapScreen: React.FC = () => {
|
|||||||
|
|
||||||
Geolocation.getCurrentPosition((pos) => {
|
Geolocation.getCurrentPosition((pos) => {
|
||||||
const { latitude, longitude } = pos.coords;
|
const { latitude, longitude } = pos.coords;
|
||||||
mapRef.current?.animateToRegion(
|
|
||||||
{
|
const newRegion = {
|
||||||
latitude,
|
latitude,
|
||||||
longitude,
|
longitude,
|
||||||
latitudeDelta: 0.05,
|
latitudeDelta: 0.02,
|
||||||
longitudeDelta: 0.05,
|
longitudeDelta: 0.02,
|
||||||
},
|
};
|
||||||
1000
|
|
||||||
);
|
setRegion(newRegion);
|
||||||
|
mapRef.current?.animateToRegion(newRegion, 1000);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* ---------------- API CALL ---------------- */
|
||||||
|
|
||||||
|
const fetchProperties = async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
'http://128.199.25.149:5000/property-search',
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
location: 'Lucknow, Uttar Pradesh, India',
|
||||||
|
radius: 500,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const json = await response.json();
|
||||||
|
|
||||||
|
if (json?.success && json?.location?.coordinates) {
|
||||||
|
setProperties(json.properties || []);
|
||||||
|
|
||||||
|
const apiRegion = {
|
||||||
|
latitude: json.location.coordinates.lat,
|
||||||
|
longitude: json.location.coordinates.lng,
|
||||||
|
latitudeDelta: 0.02,
|
||||||
|
longitudeDelta: 0.02,
|
||||||
|
};
|
||||||
|
|
||||||
|
setRegion(apiRegion);
|
||||||
|
mapRef.current?.animateToRegion(apiRegion, 1000);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.log('API Error:', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const popupTranslateY = popupAnim.interpolate({
|
const popupTranslateY = popupAnim.interpolate({
|
||||||
inputRange: [0, 1],
|
inputRange: [0, 1],
|
||||||
outputRange: [200, 0],
|
outputRange: [200, 0],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* ---------------- RENDER ---------------- */
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<MapView
|
<MapView
|
||||||
ref={mapRef}
|
ref={mapRef}
|
||||||
style={StyleSheet.absoluteFillObject}
|
style={StyleSheet.absoluteFillObject}
|
||||||
initialRegion={DEFAULT_REGION}
|
region={region} // ✅ REQUIRED
|
||||||
|
onRegionChangeComplete={(r) => {
|
||||||
|
if (!r?.longitudeDelta) return;
|
||||||
|
setRegion(r);
|
||||||
|
setMapType(r.latitudeDelta < 0.02 ? 'satellite' : 'standard');
|
||||||
|
}}
|
||||||
showsUserLocation
|
showsUserLocation
|
||||||
animationEnabled
|
animationEnabled
|
||||||
clusterPressMaxZoom={16}
|
clusterPressMaxZoom={16}
|
||||||
spiralEnabled={false}
|
spiralEnabled={false}
|
||||||
mapType={mapType}
|
mapType={mapType}
|
||||||
onRegionChangeComplete={(r) => setMapType(r.latitudeDelta < 0.02 ? 'satellite' : 'standard')}
|
|
||||||
renderCluster={(cluster, onPress) => {
|
|
||||||
const { geometry, properties } = cluster;
|
|
||||||
return (
|
|
||||||
<Marker
|
|
||||||
key={`cluster-${properties.cluster_id}`}
|
|
||||||
coordinate={{
|
|
||||||
latitude: geometry.coordinates[1],
|
|
||||||
longitude: geometry.coordinates[0],
|
|
||||||
}}
|
|
||||||
onPress={onPress}
|
|
||||||
>
|
|
||||||
<View style={styles.clusterBubble}>
|
|
||||||
<Text style={styles.clusterText}>{properties.point_count}</Text>
|
|
||||||
</View>
|
|
||||||
</Marker>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{properties.map((item) => (
|
{properties.map((item) => (
|
||||||
<PriceMarker
|
<PriceMarker
|
||||||
key={item.id}
|
key={item.id}
|
||||||
item={item}
|
item={item}
|
||||||
selected={selectedItem?.id === item.id}
|
selected={selectedItem?.id === item.id}
|
||||||
onPress={(item) => setSelectedItem(item)}
|
onPress={setSelectedItem}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</MapView>
|
</MapView>
|
||||||
|
|
||||||
{/* -------- HORIZONTAL ZILLOW-STYLE POPUP -------- */}
|
{/* -------- LOADER -------- */}
|
||||||
|
{loading && (
|
||||||
|
<View style={styles.loader}>
|
||||||
|
<ActivityIndicator size="large" />
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* -------- ZILLOW STYLE POPUP -------- */}
|
||||||
{selectedItem && (
|
{selectedItem && (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[
|
style={[
|
||||||
@@ -173,11 +208,19 @@ const MapScreen: React.FC = () => {
|
|||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
renderItem={({ item }) => (
|
renderItem={({ item }) => (
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<Image source={{ uri: item.image }} style={styles.cardImage} />
|
<Image
|
||||||
|
source={{ uri: 'https://picsum.photos/400/200' }}
|
||||||
|
style={styles.cardImage}
|
||||||
|
/>
|
||||||
<View style={styles.cardContent}>
|
<View style={styles.cardContent}>
|
||||||
<Text style={styles.cardPrice}>${formatPrice(item.price)}</Text>
|
<Text style={styles.cardPrice}>
|
||||||
|
₹{item.price_display}
|
||||||
|
</Text>
|
||||||
<Text style={styles.cardDetails}>
|
<Text style={styles.cardDetails}>
|
||||||
{item.beds} Beds • {item.baths} Baths
|
{item.bedrooms} Beds • {item.bathrooms} Baths
|
||||||
|
</Text>
|
||||||
|
<Text style={styles.cardAddress} numberOfLines={2}>
|
||||||
|
{item.address}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -190,6 +233,7 @@ const MapScreen: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/* ---------------- STYLES ---------------- */
|
/* ---------------- STYLES ---------------- */
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: { flex: 1 },
|
container: { flex: 1 },
|
||||||
|
|
||||||
@@ -201,48 +245,54 @@ const styles = StyleSheet.create({
|
|||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: '#fff',
|
borderColor: '#fff',
|
||||||
},
|
},
|
||||||
selectedPin: { backgroundColor: 'gold', borderColor: '#333' },
|
selectedPin: {
|
||||||
pinText: { color: '#fff', fontWeight: '600', fontSize: 12 },
|
backgroundColor: '#F4C430',
|
||||||
|
borderColor: '#333',
|
||||||
clusterBubble: {
|
},
|
||||||
backgroundColor: '#8B0000',
|
pinText: {
|
||||||
paddingHorizontal: 16,
|
color: '#fff',
|
||||||
paddingVertical: 10,
|
fontWeight: '600',
|
||||||
borderRadius: 24,
|
fontSize: 12,
|
||||||
borderWidth: 2,
|
|
||||||
borderColor: '#fff',
|
|
||||||
},
|
},
|
||||||
clusterText: { color: '#fff', fontWeight: 'bold', fontSize: 14 },
|
|
||||||
|
|
||||||
popupContainer: {
|
popupContainer: {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
bottom: 20,
|
bottom: 20,
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
card: {
|
card: {
|
||||||
backgroundColor: '#fff',
|
backgroundColor: '#fff',
|
||||||
width: width * 0.8,
|
width: width * 0.8,
|
||||||
marginHorizontal: 10,
|
marginHorizontal: 10,
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
elevation: 5,
|
elevation: 6,
|
||||||
},
|
},
|
||||||
cardImage: {
|
cardImage: {
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: 140,
|
height: 140,
|
||||||
},
|
},
|
||||||
cardContent: { padding: 12 },
|
cardContent: {
|
||||||
cardPrice: { fontSize: 18, fontWeight: 'bold' },
|
padding: 12,
|
||||||
cardDetails: { marginVertical: 4, color: '#666' },
|
},
|
||||||
cardBtn: {
|
cardPrice: {
|
||||||
marginTop: 8,
|
fontSize: 18,
|
||||||
backgroundColor: '#8B0000',
|
fontWeight: 'bold',
|
||||||
padding: 10,
|
},
|
||||||
borderRadius: 8,
|
cardDetails: {
|
||||||
alignItems: 'center',
|
marginVertical: 4,
|
||||||
|
color: '#555',
|
||||||
|
},
|
||||||
|
cardAddress: {
|
||||||
|
fontSize: 13,
|
||||||
|
color: '#777',
|
||||||
|
},
|
||||||
|
|
||||||
|
loader: {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '50%',
|
||||||
|
alignSelf: 'center',
|
||||||
},
|
},
|
||||||
cardBtnText: { color: '#fff', fontWeight: '600' },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default MapScreen;
|
export default MapScreen;
|
||||||
|
|||||||
248
src/screens/MapOld.tsx
Normal file
248
src/screens/MapOld.tsx
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import {
|
||||||
|
View,
|
||||||
|
StyleSheet,
|
||||||
|
PermissionsAndroid,
|
||||||
|
Platform,
|
||||||
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
|
FlatList,
|
||||||
|
Image,
|
||||||
|
Dimensions,
|
||||||
|
Animated,
|
||||||
|
} 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 { width } = Dimensions.get('window');
|
||||||
|
|
||||||
|
/* ---------------- CONSTANTS ---------------- */
|
||||||
|
const DEFAULT_REGION: Region = {
|
||||||
|
latitude: 47.6062,
|
||||||
|
longitude: -122.3321,
|
||||||
|
latitudeDelta: 0.15,
|
||||||
|
longitudeDelta: 0.15,
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---------------- MOCK DATA ---------------- */
|
||||||
|
const generateProperties = (count = 150) =>
|
||||||
|
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),
|
||||||
|
image: `https://picsum.photos/300/200?random=${i}`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const properties = generateProperties();
|
||||||
|
|
||||||
|
/* ---------------- 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 ---------------- */
|
||||||
|
const PriceMarker = ({ item, onPress, selected }: any) => {
|
||||||
|
const [tracks, setTracks] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const t = setTimeout(() => setTracks(false), 300);
|
||||||
|
return () => clearTimeout(t);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Marker
|
||||||
|
coordinate={{ latitude: item.lat, longitude: item.lng }}
|
||||||
|
tracksViewChanges={tracks}
|
||||||
|
onPress={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onPress(item);
|
||||||
|
}}
|
||||||
|
pinColor={selected ? 'gold' : '#8B0000'}
|
||||||
|
>
|
||||||
|
<View style={[styles.pin, selected && styles.selectedPin]}>
|
||||||
|
<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);
|
||||||
|
const [popupAnim] = useState(new Animated.Value(0));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
locateUser();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Animated.timing(popupAnim, {
|
||||||
|
toValue: selectedItem ? 1 : 0,
|
||||||
|
duration: 300,
|
||||||
|
useNativeDriver: true,
|
||||||
|
}).start();
|
||||||
|
}, [selectedItem]);
|
||||||
|
|
||||||
|
const locateUser = async () => {
|
||||||
|
if (Platform.OS === 'android') {
|
||||||
|
const granted = await PermissionsAndroid.request(
|
||||||
|
PermissionsAndroid.PERMISSIONS.ACCESS_FINE_LOCATION
|
||||||
|
);
|
||||||
|
if (granted !== PermissionsAndroid.RESULTS.GRANTED) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Geolocation.getCurrentPosition((pos) => {
|
||||||
|
const { latitude, longitude } = pos.coords;
|
||||||
|
mapRef.current?.animateToRegion(
|
||||||
|
{
|
||||||
|
latitude,
|
||||||
|
longitude,
|
||||||
|
latitudeDelta: 0.05,
|
||||||
|
longitudeDelta: 0.05,
|
||||||
|
},
|
||||||
|
1000
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const popupTranslateY = popupAnim.interpolate({
|
||||||
|
inputRange: [0, 1],
|
||||||
|
outputRange: [200, 0],
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={styles.container}>
|
||||||
|
<MapView
|
||||||
|
ref={mapRef}
|
||||||
|
style={StyleSheet.absoluteFillObject}
|
||||||
|
initialRegion={DEFAULT_REGION}
|
||||||
|
showsUserLocation
|
||||||
|
animationEnabled
|
||||||
|
clusterPressMaxZoom={16}
|
||||||
|
spiralEnabled={false}
|
||||||
|
mapType={mapType}
|
||||||
|
onRegionChangeComplete={(r) => setMapType(r.latitudeDelta < 0.02 ? 'satellite' : 'standard')}
|
||||||
|
renderCluster={(cluster, onPress) => {
|
||||||
|
const { geometry, properties } = cluster;
|
||||||
|
return (
|
||||||
|
<Marker
|
||||||
|
key={`cluster-${properties.cluster_id}`}
|
||||||
|
coordinate={{
|
||||||
|
latitude: geometry.coordinates[1],
|
||||||
|
longitude: geometry.coordinates[0],
|
||||||
|
}}
|
||||||
|
onPress={onPress}
|
||||||
|
>
|
||||||
|
<View style={styles.clusterBubble}>
|
||||||
|
<Text style={styles.clusterText}>{properties.point_count}</Text>
|
||||||
|
</View>
|
||||||
|
</Marker>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{properties.map((item) => (
|
||||||
|
<PriceMarker
|
||||||
|
key={item.id}
|
||||||
|
item={item}
|
||||||
|
selected={selectedItem?.id === item.id}
|
||||||
|
onPress={(item) => setSelectedItem(item)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</MapView>
|
||||||
|
|
||||||
|
{/* -------- HORIZONTAL ZILLOW-STYLE POPUP -------- */}
|
||||||
|
{selectedItem && (
|
||||||
|
<Animated.View
|
||||||
|
style={[
|
||||||
|
styles.popupContainer,
|
||||||
|
{ transform: [{ translateY: popupTranslateY }] },
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<FlatList
|
||||||
|
horizontal
|
||||||
|
data={[selectedItem]}
|
||||||
|
keyExtractor={(item) => item.id}
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
renderItem={({ item }) => (
|
||||||
|
<View style={styles.card}>
|
||||||
|
<Image source={{ uri: item.image }} style={styles.cardImage} />
|
||||||
|
<View style={styles.cardContent}>
|
||||||
|
<Text style={styles.cardPrice}>${formatPrice(item.price)}</Text>
|
||||||
|
<Text style={styles.cardDetails}>
|
||||||
|
{item.beds} Beds • {item.baths} Baths
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Animated.View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* ---------------- STYLES ---------------- */
|
||||||
|
const styles = StyleSheet.create({
|
||||||
|
container: { flex: 1 },
|
||||||
|
|
||||||
|
pin: {
|
||||||
|
backgroundColor: '#8B0000',
|
||||||
|
paddingHorizontal: 12,
|
||||||
|
paddingVertical: 6,
|
||||||
|
borderRadius: 20,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderColor: '#fff',
|
||||||
|
},
|
||||||
|
selectedPin: { backgroundColor: 'gold', borderColor: '#333' },
|
||||||
|
pinText: { color: '#fff', fontWeight: '600', fontSize: 12 },
|
||||||
|
|
||||||
|
clusterBubble: {
|
||||||
|
backgroundColor: '#8B0000',
|
||||||
|
paddingHorizontal: 16,
|
||||||
|
paddingVertical: 10,
|
||||||
|
borderRadius: 24,
|
||||||
|
borderWidth: 2,
|
||||||
|
borderColor: '#fff',
|
||||||
|
},
|
||||||
|
clusterText: { color: '#fff', fontWeight: 'bold', fontSize: 14 },
|
||||||
|
|
||||||
|
popupContainer: {
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: 20,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
backgroundColor: '#fff',
|
||||||
|
width: width * 0.8,
|
||||||
|
marginHorizontal: 10,
|
||||||
|
borderRadius: 16,
|
||||||
|
overflow: 'hidden',
|
||||||
|
elevation: 5,
|
||||||
|
},
|
||||||
|
cardImage: {
|
||||||
|
width: '100%',
|
||||||
|
height: 140,
|
||||||
|
},
|
||||||
|
cardContent: { padding: 12 },
|
||||||
|
cardPrice: { fontSize: 18, fontWeight: 'bold' },
|
||||||
|
cardDetails: { marginVertical: 4, color: '#666' },
|
||||||
|
cardBtn: {
|
||||||
|
marginTop: 8,
|
||||||
|
backgroundColor: '#8B0000',
|
||||||
|
padding: 10,
|
||||||
|
borderRadius: 8,
|
||||||
|
alignItems: 'center',
|
||||||
|
},
|
||||||
|
cardBtnText: { color: '#fff', fontWeight: '600' },
|
||||||
|
});
|
||||||
|
|
||||||
|
export default MapScreen;
|
||||||
Reference in New Issue
Block a user