Код: Выделить всё
import React, {useRef, useState, useEffect, useMemo, useCallback} from 'react';
import {
View,
Text,
StyleSheet,
Dimensions,
Animated,
PanResponder,
LayoutChangeEvent,
ActivityIndicator,
Platform,
Image,
TouchableOpacity,
ScrollView,
ViewStyle,
TextStyleIOS,
StatusBar,
ImageBackground,
} from 'react-native';
import {offsetOverlappingCoordinates} from '../utils/offSetSameCoordinate';
import NetInfo from '@react-native-community/netinfo';
import MapView, {Marker, Region} from 'react-native-maps';
import HeaderUser from '../components/HeaderUser';
import EventCard from '../components/EventCard';
import {config} from '../constants/config';
import SearchBottomSheet, {
SearchBottomSheetRef,
} from '../components/SearchBottomSheet';
import {
useGetUserNearbyEventsQuery,
useGetEventByIdQuery,
} from '../services/api/eventApi';
import {useRoute, RouteProp, useNavigation} from '@react-navigation/native';
import {EnumDuration, IEvent, IEventArtist, IGetNearbyEventParams} from '../types.d';
import {getGenreImage, getGenreBackgroundImage} from '../utils/getGenresImage';
import {createClusters} from '../utils/mapScreen';
import {requestUserLocation} from '../utils/locationHelper';
import AppImage from '../components/BaseImage';
const {height, width} = Dimensions.get('window');
const STATUS_BAR_HEIGHT =
StatusBar.currentHeight || (Platform.OS === 'ios' ? 44 : 0);
const HEADER_HEIGHT = 80;
const SAFE_TOP_MARGIN = STATUS_BAR_HEIGHT + HEADER_HEIGHT - 70;
const SNAP_BOTTOM = height - 170;
const MAX_SHEET_HEIGHT = height {
const route = useRoute();
const {
eventId,
showSpecificEvent,
latitude: paramLatitude,
longitude: paramLongitude,
} = route.params || {};
const mapRef = useRef(null);
const scrollViewRef = useRef(null);
const scrollY = useRef(0);
const [locationFetched, setLocationFetched] = useState(false);
const bottomSheetHeight = useRef(0);
const isDraggingSheet = useRef(false);
const isScrolling = useRef(false);
const [deviceSize, setDeviceSize] = useState({width, height});
const [singleEventCardHeight, setSingleEventCardHeight] = useState(
Math.min(250, height * (height {
setIsInitialMarkerLoad(false);
}, 1000);
return () => clearTimeout(timer);
}, [apiEvents, specificEventData, mapIEventToEvent, showSpecificEvent]);
useEffect(() => {
if (error || specificEventError) {
console.error('Error fetching events:', error || specificEventError);
setShowNoResultsMessage(true);
}
}, [error, specificEventError]);
const panY = useRef(new Animated.Value(SNAP_BOTTOM)).current;
const [isSheetOpen, setIsSheetOpen] = useState(false);
const clusterArray = useMemo(() => {
if (showSpecificEvent) {
return [];
}
return createClusters(filteredEvents, currentZoomLevel);
}, [filteredEvents, currentZoomLevel, showSpecificEvent]);
const filteredClusters = useMemo(() => {
if (showSpecificEvent || !filtersApplied) {
return createClusters(events, currentZoomLevel);
}
return clusterArray;
}, [
events,
clusterArray,
filtersApplied,
currentZoomLevel,
showSpecificEvent,
]);
useEffect(() => {
if (showSpecificEvent) {
return;
}
if (viewMode === 'clusters') {
setVisibleEvents(filteredEvents);
} else if (activeClusterIndex !== null) {
const currentCluster = filteredClusters[activeClusterIndex];
if (currentCluster) {
setVisibleEvents(currentCluster.events);
}
}
}, [
filtersApplied,
filteredClusters,
filteredEvents,
viewMode,
activeClusterIndex,
showSpecificEvent,
]);
useEffect(() => {
if (filtersApplied && filteredEvents.length === 0 && !showSpecificEvent) {
setShowNoResultsMessage(true);
} else {
setShowNoResultsMessage(false);
}
}, [filtersApplied, filteredEvents, showSpecificEvent]);
const expandToFullScreen = () => {
Animated.spring(panY, {
toValue: TOP_CONSTRAINT,
useNativeDriver: false,
tension: 70,
friction: 12,
}).start(() => setIsSheetOpen(true));
};
const handleClusterPress = (clusterIndex: number) => {
if (showSpecificEvent) {
return;
}
setActiveClusterIndex(clusterIndex);
setViewMode('events');
if (isSheetOpen) {
closeBottomSheet();
setSelectedEventIndex(null);
}
const cluster = filteredClusters[clusterIndex];
if (mapRef.current) {
mapRef.current.animateToRegion(
{
latitude: cluster.coordinate.latitude,
longitude: cluster.coordinate.longitude,
latitudeDelta: 0.03,
longitudeDelta: 0.03,
},
1000,
);
}
setVisibleEvents(cluster.events);
if (cluster.events.length > 3) {
expandToFullScreen();
} else {
openBottomSheet();
}
};
const handleEventMarkerPress = (eventIndex: number) => {
setIsLoading(true);
setSelectedEventIndex(eventIndex);
const currentEvent = visibleEvents[eventIndex];
if (mapRef.current) {
mapRef.current.animateToRegion(
{
latitude: currentEvent.coordinate.latitude,
longitude: currentEvent.coordinate.longitude,
latitudeDelta: 0.001,
longitudeDelta: 0.001,
},
500,
);
}
const targetHeight = determineBottomSheetHeight();
Animated.spring(panY, {
toValue: targetHeight,
useNativeDriver: false,
tension: 70,
friction: 12,
}).start(() => setIsSheetOpen(true));
setTimeout(() => {
setIsLoading(false);
}, 250);
};
const handleCenterToUserLocation = async () => {
try {
setIsLoading(true);
const userRegion = await requestUserLocation();
setUserLocation(userRegion);
if (mapRef.current) {
mapRef.current.animateToRegion(
{
latitude: userRegion.latitude,
longitude: userRegion.longitude,
latitudeDelta: 0.0009,
longitudeDelta: 0.0009,
},
1000,
);
}
} catch (locationError) {
console.error('Failed to fetch user location:', locationError);
} finally {
setIsLoading(false);
}
};
const handleRegionChange = (region: Region) => {
if (showSpecificEvent) {
return;
}
setCurrentZoomLevel(region.latitudeDelta);
if (region.latitudeDelta > 0.05 && viewMode === 'events') {
setViewMode('clusters');
setVisibleEvents(filteredEvents);
setActiveClusterIndex(null);
if (isSheetOpen) {
closeBottomSheet();
setSelectedEventIndex(null);
}
setSelectedEventIndex(null);
} else if (region.latitudeDelta {
const distance = Math.sqrt(
Math.pow(region.latitude - cluster.coordinate.latitude, 2) +
Math.pow(region.longitude - cluster.coordinate.longitude, 2),
);
if (distance < minDistance) {
minDistance = distance;
closestClusterIndex = index;
}
});
if (closestClusterIndex !== -1 && minDistance < 0.02) {
setActiveClusterIndex(closestClusterIndex);
setViewMode('events');
if (isSheetOpen) {
closeBottomSheet();
setSelectedEventIndex(null);
}
const cluster = filteredClusters[closestClusterIndex];
setVisibleEvents(cluster.events);
// Don't automatically open or expand the bottom sheet on zoom
// This prevents the sheet from opening when zooming in
}
}
};
const handleScrollBegin = () => {
isScrolling.current = true;
};
const handleScrollEnd = () => {
isScrolling.current = false;
};
const handleScroll = (event: any) => {
scrollY.current = event.nativeEvent.contentOffset.y;
const eventsToShow = isSearchActive
? events.filter(event => {
const lowercaseQuery = searchQuery.toLowerCase();
return (
event.location.toLowerCase().includes(lowercaseQuery) ||
event.name.toLowerCase().includes(lowercaseQuery)
);
})
: filtersApplied
? filteredEvents
: viewMode === 'events'
? visibleEvents
: events;
if (
eventsToShow.length > 3 &&
isSheetOpen &&
selectedEventIndex === null &&
!showSpecificEvent
) {
expandToFullScreen();
}
};
const handleBottomSheetLayout = (event: LayoutChangeEvent) => {
bottomSheetHeight.current = event.nativeEvent.layout.height;
};
const panResponder = useRef(
PanResponder.create({
onMoveShouldSetPanResponder: (evt, gestureState) => {
const isSignificantVerticalGesture =
Math.abs(gestureState.dy) > 10 &&
Math.abs(gestureState.dy) > Math.abs(gestureState.dx);
if (!isSignificantVerticalGesture) {
return false;
}
const isDraggingHandle = evt.nativeEvent.locationY < 60;
const isAtTopOfScrollView = scrollY.current 0;
return isDraggingHandle || (isAtTopOfScrollView && isDraggingDown);
},
onPanResponderGrant: () => {
panY.setOffset(panY._value);
panY.setValue(0);
isDraggingSheet.current = true;
},
onPanResponderMove: (_, gestureState) => {
let newY = gestureState.dy;
const minHeight = determineBottomSheetHeight();
const maxHeight = SNAP_BOTTOM;
const currentPosition = panY._offset + newY;
if (currentPosition < minHeight) {
newY = minHeight - panY._offset;
} else if (currentPosition > maxHeight) {
newY = maxHeight - panY._offset;
}
panY.setValue(newY);
},
onPanResponderRelease: (_, gestureState) => {
panY.flattenOffset();
isDraggingSheet.current = false;
const currentPosition = panY._value;
const velocity = gestureState.vy;
const shouldOpen =
velocity < -0.3 ||
(velocity > -0.3 &&
velocity < 0.3 &&
currentPosition < SNAP_BOTTOM / 2);
const targetValue = shouldOpen
? determineBottomSheetHeight()
: SNAP_BOTTOM;
Animated.spring(panY, {
toValue: targetValue,
useNativeDriver: false,
tension: 70,
friction: 12,
}).start(() => {
setIsSheetOpen(shouldOpen);
});
},
onPanResponderTerminate: (_, _gestureState) => {
panY.flattenOffset();
isDraggingSheet.current = false;
const currentPosition = panY._value;
const shouldOpen = currentPosition < SNAP_BOTTOM / 2;
const targetValue = shouldOpen
? determineBottomSheetHeight()
: SNAP_BOTTOM;
Animated.spring(panY, {
toValue: targetValue,
useNativeDriver: false,
tension: 70,
friction: 12,
}).start(() => {
setIsSheetOpen(shouldOpen);
});
},
}),
).current;
const handleEventCardLayout = (event: LayoutChangeEvent) => {
const {height: cardHeight} = event.nativeEvent.layout;
const isSmallDevice = deviceSize.height {
if (filtersApplied && filteredEvents.length === 0) {
return;
}
if (showSpecificEvent) {
return;
}
setSelectedEventIndex(null);
const eventsToShow = isSearchActive
? events.filter(event => {
const lowercaseQuery = searchQuery.toLowerCase();
return (
event.location.toLowerCase().includes(lowercaseQuery) ||
event.name.toLowerCase().includes(lowercaseQuery)
);
})
: filtersApplied
? filteredEvents
: viewMode === 'events'
? visibleEvents
: events;
if (eventsToShow.length > 3) {
expandToFullScreen();
} else {
openBottomSheet();
}
};
const handleSearch = () => {
if (!searchQuery) {
setIsSearchActive(false);
}
searchBottomSheetRef.current?.open();
};
const handleSelectSearchResult = (result: string) => {
setSearchQuery(result);
setIsSearchActive(true);
let matchedEvents = 0;
if (result) {
const lowercaseResult = result.toLowerCase();
matchedEvents = events.filter(
event =>
event.location.toLowerCase().includes(lowercaseResult) ||
event.name.toLowerCase().includes(lowercaseResult),
).length;
}
setSearchResultsCount(matchedEvents);
if (matchedEvents > 0) {
setSelectedEventIndex(null);
if (matchedEvents > 3) {
expandToFullScreen();
} else {
openBottomSheet();
}
}
};
const handleMapReady = () => {
setMapReady(true); // Trigger image re-render
};
// Fallback image source
const defaultGenreImage = {uri: 'https://via.placeholder.com/66'};
const getSafeGenreImage = useCallback((genre: string) => {
const imageSource = getGenreImage(genre);
if (!imageSource || (typeof imageSource === 'object' && !imageSource.uri)) {
console.warn(`Invalid genre image for ${genre}, using default`);
return defaultGenreImage;
}
return imageSource;
}, []);
const getSafeGenreBackgroundImage = useCallback((genre: string) => {
const backgroundImage = getGenreBackgroundImage(genre);
if (
!backgroundImage ||
(typeof backgroundImage === 'object' && !backgroundImage.uri)
) {
console.warn(
`Invalid genre background image for ${genre}, using checkmap fallback`,
);
return require('../assets/checkmap.png'); // fallback to your original image
}
return backgroundImage;
}, []);
const currentEvent =
selectedEventIndex !== null && viewMode === 'events'
? visibleEvents[selectedEventIndex]
: null;
const visibleEventsCount = filteredEvents.length;
const isLoadingEvents = showSpecificEvent
? isFetchingSpecificEvent
: isFetchingEvents;
return (
{isLoading && !locationFetched && (
Getting your location...
)}
{mapReady &&
isConnected &&
viewMode === 'clusters' &&
!showSpecificEvent &&
filteredClusters.map((cluster, index) => (
(markerRefs.current[`cluster-${index}`] = ref)} // Add ref
coordinate={cluster.coordinate}
onPress={() => handleClusterPress(index)}
tracksViewChanges={isInitialMarkerLoad}>
{cluster.count}
))}
{mapReady &&
isConnected &&
(viewMode === 'events' || showSpecificEvent) &&
(() => {
const adjustedCoordinates =
offsetOverlappingCoordinates(visibleEvents);
return visibleEvents.map((event, index) => {
const isSelected = index === selectedEventIndex;
const isZoomedIn = currentZoomLevel
{showCallout ? (
{event.name}
{event.artist?.map((artist, artistIndex) => (
{artist.name}
))}
{event.venue?.map((venue, venueIndex) => (
{venue.name}
))}
) : (
{1}
)}
);
});
})()}
{
if (
event.nativeEvent.layout.height > 0 &&
doubleEventCardHeight === 520
) {
const isSmallDevice = deviceSize.height
{(() => {
const eventsToShow = showSpecificEvent
? events
: isSearchActive
? events.filter(event => {
const lowercaseQuery = searchQuery.toLowerCase();
return (
event.location.toLowerCase().includes(lowercaseQuery) ||
event.name.toLowerCase().includes(lowercaseQuery)
);
})
: filtersApplied
? filteredEvents
: viewMode === 'events'
? visibleEvents
: events;
if (eventsToShow.length === 0) {
return (
No events found
);
} else if (eventsToShow.length === 1) {
const singleEvent = eventsToShow[0];
return (
);
}
return eventsToShow.map((event, index) => (
));
})()}
)
) : null}
{!showSpecificEvent && (
)}
);
};
export default MapScreenUser;
Подробнее здесь: https://stackoverflow.com/questions/797 ... ted-on-ios