diff --git a/src/screens/Map.tsx b/src/screens/Map.tsx index 3b2e476..2f5e427 100644 --- a/src/screens/Map.tsx +++ b/src/screens/Map.tsx @@ -6,17 +6,18 @@ import { 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 { height } = Dimensions.get('window'); +const { width } = Dimensions.get('window'); /* ---------------- CONSTANTS ---------------- */ - const DEFAULT_REGION: Region = { latitude: 47.6062, longitude: -122.3321, @@ -25,8 +26,7 @@ const DEFAULT_REGION: Region = { }; /* ---------------- MOCK DATA ---------------- */ - -const generateProperties = (count = 1500) => +const generateProperties = (count = 150) => Array.from({ length: count }).map((_, i) => ({ id: `${i}`, lat: 47.5 + Math.random() * 0.25, @@ -34,12 +34,12 @@ const generateProperties = (count = 1500) => 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`; @@ -47,8 +47,7 @@ const formatPrice = (value: number) => { }; /* ---------------- PRICE MARKER ---------------- */ - -const PriceMarker = ({ item, onPress }: any) => { +const PriceMarker = ({ item, onPress, selected }: any) => { const [tracks, setTracks] = useState(true); useEffect(() => { @@ -61,11 +60,12 @@ const PriceMarker = ({ item, onPress }: any) => { coordinate={{ latitude: item.lat, longitude: item.lng }} tracksViewChanges={tracks} onPress={(e) => { - e.stopPropagation(); // 🔥 prevents map press - onPress(); + e.stopPropagation(); + onPress(item); }} + pinColor={selected ? 'gold' : '#8B0000'} > - + {formatPrice(item.price)} @@ -73,18 +73,24 @@ const PriceMarker = ({ item, onPress }: any) => { }; /* ---------------- MAIN SCREEN ---------------- */ - const MapScreen: React.FC = () => { const mapRef = useRef(null); - const [mapType, setMapType] = useState('standard'); const [selectedItem, setSelectedItem] = useState(null); + const [popupAnim] = useState(new Animated.Value(0)); - /* ---- current location ---- */ 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( @@ -93,7 +99,7 @@ const MapScreen: React.FC = () => { if (granted !== PermissionsAndroid.RESULTS.GRANTED) return; } - Geolocation.getCurrentPosition(pos => { + Geolocation.getCurrentPosition((pos) => { const { latitude, longitude } = pos.coords; mapRef.current?.animateToRegion( { @@ -107,6 +113,11 @@ const MapScreen: React.FC = () => { }); }; + const popupTranslateY = popupAnim.interpolate({ + inputRange: [0, 1], + outputRange: [200, 0], + }); + return ( { clusterPressMaxZoom={16} spiralEnabled={false} mapType={mapType} - onRegionChangeComplete={(r) => { - setSelectedItem(null); - setMapType(r.latitudeDelta < 0.02 ? 'satellite' : 'standard'); - }} - - /* ---- CLUSTER ---- */ + onRegionChangeComplete={(r) => setMapType(r.latitudeDelta < 0.02 ? 'satellite' : 'standard')} renderCluster={(cluster, onPress) => { const { geometry, properties } = cluster; - return ( { onPress={onPress} > - - {properties.point_count} - + {properties.point_count} ); }} > - {properties.map(item => ( + {properties.map((item) => ( setSelectedItem(item)} + selected={selectedItem?.id === item.id} + onPress={(item) => setSelectedItem(item)} /> ))} - {/* -------- BOTTOM POPUP -------- */} + {/* -------- HORIZONTAL ZILLOW-STYLE POPUP -------- */} {selectedItem && ( - - setSelectedItem(null)} - > - ✕ - - - - ${formatPrice(selectedItem.price)} - - - - {selectedItem.beds} Beds • {selectedItem.baths} Baths - - - - View Details - - + + item.id} + showsHorizontalScrollIndicator={false} + renderItem={({ item }) => ( + + + + ${formatPrice(item.price)} + + {item.beds} Beds • {item.baths} Baths + + + + )} + /> + )} ); }; /* ---------------- STYLES ---------------- */ - const styles = StyleSheet.create({ container: { flex: 1 }, pin: { backgroundColor: '#8B0000', - paddingHorizontal: 10, + paddingHorizontal: 12, paddingVertical: 6, - borderRadius: 18, - }, - pinText: { - color: '#fff', - fontWeight: '600', - fontSize: 12, + borderRadius: 20, + borderWidth: 1, + borderColor: '#fff', }, + selectedPin: { backgroundColor: 'gold', borderColor: '#333' }, + pinText: { color: '#fff', fontWeight: '600', fontSize: 12 }, clusterBubble: { backgroundColor: '#8B0000', - paddingHorizontal: 14, - paddingVertical: 8, + paddingHorizontal: 16, + paddingVertical: 10, borderRadius: 24, borderWidth: 2, borderColor: '#fff', }, - clusterText: { - color: '#fff', - fontWeight: 'bold', - fontSize: 13, - }, + clusterText: { color: '#fff', fontWeight: 'bold', fontSize: 14 }, - popup: { + popupContainer: { position: 'absolute', bottom: 20, - left: 16, - right: 16, + left: 0, + right: 0, + }, + card: { backgroundColor: '#fff', + width: width * 0.8, + marginHorizontal: 10, borderRadius: 16, - padding: 16, - elevation: 10, + overflow: 'hidden', + elevation: 5, }, - close: { - position: 'absolute', - right: 12, - top: 12, + cardImage: { + width: '100%', + height: 140, }, - price: { - fontSize: 22, - fontWeight: 'bold', - }, - details: { - marginVertical: 6, - color: '#666', - }, - btn: { - marginTop: 12, + cardContent: { padding: 12 }, + cardPrice: { fontSize: 18, fontWeight: 'bold' }, + cardDetails: { marginVertical: 4, color: '#666' }, + cardBtn: { + marginTop: 8, backgroundColor: '#8B0000', - padding: 12, - borderRadius: 10, + padding: 10, + borderRadius: 8, alignItems: 'center', }, - btnText: { - color: '#fff', - fontWeight: '600', - }, + cardBtnText: { color: '#fff', fontWeight: '600' }, }); export default MapScreen;