Added interactive property map with markers and floating popups showing price, beds/baths, and View Details button

This commit is contained in:
2026-01-02 23:51:18 +05:30
parent 688f86d595
commit c1030874e3

View File

@@ -6,17 +6,18 @@ import {
Platform, Platform,
Text, Text,
TouchableOpacity, TouchableOpacity,
FlatList,
Image,
Dimensions, Dimensions,
Animated,
} 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 { height } = Dimensions.get('window'); const { width } = Dimensions.get('window');
/* ---------------- CONSTANTS ---------------- */ /* ---------------- CONSTANTS ---------------- */
const DEFAULT_REGION: Region = { const DEFAULT_REGION: Region = {
latitude: 47.6062, latitude: 47.6062,
longitude: -122.3321, longitude: -122.3321,
@@ -25,8 +26,7 @@ const DEFAULT_REGION: Region = {
}; };
/* ---------------- MOCK DATA ---------------- */ /* ---------------- MOCK DATA ---------------- */
const generateProperties = (count = 150) =>
const generateProperties = (count = 1500) =>
Array.from({ length: count }).map((_, i) => ({ Array.from({ length: count }).map((_, i) => ({
id: `${i}`, id: `${i}`,
lat: 47.5 + Math.random() * 0.25, lat: 47.5 + Math.random() * 0.25,
@@ -34,12 +34,12 @@ const generateProperties = (count = 1500) =>
price: Math.floor(300000 + Math.random() * 4000000), price: Math.floor(300000 + Math.random() * 4000000),
beds: Math.floor(1 + Math.random() * 5), beds: Math.floor(1 + Math.random() * 5),
baths: Math.floor(1 + Math.random() * 4), baths: Math.floor(1 + Math.random() * 4),
image: `https://picsum.photos/300/200?random=${i}`,
})); }));
const properties = generateProperties(); const properties = generateProperties();
/* ---------------- HELPERS ---------------- */ /* ---------------- HELPERS ---------------- */
const formatPrice = (value: number) => { const formatPrice = (value: number) => {
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`;
@@ -47,8 +47,7 @@ const formatPrice = (value: number) => {
}; };
/* ---------------- PRICE MARKER ---------------- */ /* ---------------- PRICE MARKER ---------------- */
const PriceMarker = ({ item, onPress, selected }: any) => {
const PriceMarker = ({ item, onPress }: any) => {
const [tracks, setTracks] = useState(true); const [tracks, setTracks] = useState(true);
useEffect(() => { useEffect(() => {
@@ -61,11 +60,12 @@ const PriceMarker = ({ item, onPress }: any) => {
coordinate={{ latitude: item.lat, longitude: item.lng }} coordinate={{ latitude: item.lat, longitude: item.lng }}
tracksViewChanges={tracks} tracksViewChanges={tracks}
onPress={(e) => { onPress={(e) => {
e.stopPropagation(); // 🔥 prevents map press e.stopPropagation();
onPress(); onPress(item);
}} }}
pinColor={selected ? 'gold' : '#8B0000'}
> >
<View style={styles.pin}> <View style={[styles.pin, selected && styles.selectedPin]}>
<Text style={styles.pinText}>{formatPrice(item.price)}</Text> <Text style={styles.pinText}>{formatPrice(item.price)}</Text>
</View> </View>
</Marker> </Marker>
@@ -73,18 +73,24 @@ const PriceMarker = ({ item, onPress }: any) => {
}; };
/* ---------------- MAIN SCREEN ---------------- */ /* ---------------- MAIN SCREEN ---------------- */
const MapScreen: React.FC = () => { const MapScreen: React.FC = () => {
const mapRef = useRef<MapView>(null); const mapRef = useRef<MapView>(null);
const [mapType, setMapType] = useState<MapType>('standard'); const [mapType, setMapType] = useState<MapType>('standard');
const [selectedItem, setSelectedItem] = useState<any>(null); const [selectedItem, setSelectedItem] = useState<any>(null);
const [popupAnim] = useState(new Animated.Value(0));
/* ---- current location ---- */
useEffect(() => { useEffect(() => {
locateUser(); locateUser();
}, []); }, []);
useEffect(() => {
Animated.timing(popupAnim, {
toValue: selectedItem ? 1 : 0,
duration: 300,
useNativeDriver: true,
}).start();
}, [selectedItem]);
const locateUser = async () => { const locateUser = async () => {
if (Platform.OS === 'android') { if (Platform.OS === 'android') {
const granted = await PermissionsAndroid.request( const granted = await PermissionsAndroid.request(
@@ -93,7 +99,7 @@ const MapScreen: React.FC = () => {
if (granted !== PermissionsAndroid.RESULTS.GRANTED) return; if (granted !== PermissionsAndroid.RESULTS.GRANTED) return;
} }
Geolocation.getCurrentPosition(pos => { Geolocation.getCurrentPosition((pos) => {
const { latitude, longitude } = pos.coords; const { latitude, longitude } = pos.coords;
mapRef.current?.animateToRegion( mapRef.current?.animateToRegion(
{ {
@@ -107,6 +113,11 @@ const MapScreen: React.FC = () => {
}); });
}; };
const popupTranslateY = popupAnim.interpolate({
inputRange: [0, 1],
outputRange: [200, 0],
});
return ( return (
<View style={styles.container}> <View style={styles.container}>
<MapView <MapView
@@ -118,15 +129,9 @@ const MapScreen: React.FC = () => {
clusterPressMaxZoom={16} clusterPressMaxZoom={16}
spiralEnabled={false} spiralEnabled={false}
mapType={mapType} mapType={mapType}
onRegionChangeComplete={(r) => { onRegionChangeComplete={(r) => setMapType(r.latitudeDelta < 0.02 ? 'satellite' : 'standard')}
setSelectedItem(null);
setMapType(r.latitudeDelta < 0.02 ? 'satellite' : 'standard');
}}
/* ---- CLUSTER ---- */
renderCluster={(cluster, onPress) => { renderCluster={(cluster, onPress) => {
const { geometry, properties } = cluster; const { geometry, properties } = cluster;
return ( return (
<Marker <Marker
key={`cluster-${properties.cluster_id}`} key={`cluster-${properties.cluster_id}`}
@@ -137,115 +142,107 @@ const MapScreen: React.FC = () => {
onPress={onPress} onPress={onPress}
> >
<View style={styles.clusterBubble}> <View style={styles.clusterBubble}>
<Text style={styles.clusterText}> <Text style={styles.clusterText}>{properties.point_count}</Text>
{properties.point_count}
</Text>
</View> </View>
</Marker> </Marker>
); );
}} }}
> >
{properties.map(item => ( {properties.map((item) => (
<PriceMarker <PriceMarker
key={item.id} key={item.id}
item={item} item={item}
onPress={() => setSelectedItem(item)} selected={selectedItem?.id === item.id}
onPress={(item) => setSelectedItem(item)}
/> />
))} ))}
</MapView> </MapView>
{/* -------- BOTTOM POPUP -------- */} {/* -------- HORIZONTAL ZILLOW-STYLE POPUP -------- */}
{selectedItem && ( {selectedItem && (
<View style={styles.popup}> <Animated.View
<TouchableOpacity style={[
style={styles.close} styles.popupContainer,
onPress={() => setSelectedItem(null)} { transform: [{ translateY: popupTranslateY }] },
]}
> >
<Text></Text> <FlatList
</TouchableOpacity> horizontal
data={[selectedItem]}
<Text style={styles.price}> keyExtractor={(item) => item.id}
${formatPrice(selectedItem.price)} 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> </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>
)}
/>
</Animated.View>
)} )}
</View> </View>
); );
}; };
/* ---------------- STYLES ---------------- */ /* ---------------- STYLES ---------------- */
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { flex: 1 }, container: { flex: 1 },
pin: { pin: {
backgroundColor: '#8B0000', backgroundColor: '#8B0000',
paddingHorizontal: 10, paddingHorizontal: 12,
paddingVertical: 6, paddingVertical: 6,
borderRadius: 18, borderRadius: 20,
}, borderWidth: 1,
pinText: { borderColor: '#fff',
color: '#fff',
fontWeight: '600',
fontSize: 12,
}, },
selectedPin: { backgroundColor: 'gold', borderColor: '#333' },
pinText: { color: '#fff', fontWeight: '600', fontSize: 12 },
clusterBubble: { clusterBubble: {
backgroundColor: '#8B0000', backgroundColor: '#8B0000',
paddingHorizontal: 14, paddingHorizontal: 16,
paddingVertical: 8, paddingVertical: 10,
borderRadius: 24, borderRadius: 24,
borderWidth: 2, borderWidth: 2,
borderColor: '#fff', borderColor: '#fff',
}, },
clusterText: { clusterText: { color: '#fff', fontWeight: 'bold', fontSize: 14 },
color: '#fff',
fontWeight: 'bold',
fontSize: 13,
},
popup: { popupContainer: {
position: 'absolute', position: 'absolute',
bottom: 20, bottom: 20,
left: 16, left: 0,
right: 16, right: 0,
},
card: {
backgroundColor: '#fff', backgroundColor: '#fff',
width: width * 0.8,
marginHorizontal: 10,
borderRadius: 16, borderRadius: 16,
padding: 16, overflow: 'hidden',
elevation: 10, elevation: 5,
}, },
close: { cardImage: {
position: 'absolute', width: '100%',
right: 12, height: 140,
top: 12,
}, },
price: { cardContent: { padding: 12 },
fontSize: 22, cardPrice: { fontSize: 18, fontWeight: 'bold' },
fontWeight: 'bold', cardDetails: { marginVertical: 4, color: '#666' },
}, cardBtn: {
details: { marginTop: 8,
marginVertical: 6,
color: '#666',
},
btn: {
marginTop: 12,
backgroundColor: '#8B0000', backgroundColor: '#8B0000',
padding: 12, padding: 10,
borderRadius: 10, borderRadius: 8,
alignItems: 'center', alignItems: 'center',
}, },
btnText: { cardBtnText: { color: '#fff', fontWeight: '600' },
color: '#fff',
fontWeight: '600',
},
}); });
export default MapScreen; export default MapScreen;