|
@@ -1,4 +1,13 @@
|
|
|
-import { Animated, Linking, Platform, Text, TouchableOpacity, View } from 'react-native';
|
|
|
+import {
|
|
|
+ Animated as Animation,
|
|
|
+ Dimensions,
|
|
|
+ Linking,
|
|
|
+ Platform,
|
|
|
+ Text,
|
|
|
+ TextInput,
|
|
|
+ TouchableOpacity,
|
|
|
+ View
|
|
|
+} from 'react-native';
|
|
|
import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react';
|
|
|
import MapView, { Geojson, Marker, UrlTile } from 'react-native-maps';
|
|
|
import * as turf from '@turf/turf';
|
|
@@ -41,13 +50,18 @@ import { usePostSetNmRegionMutation } from '@api/myRegions';
|
|
|
import { usePostSetDareRegionMutation } from '@api/myDARE';
|
|
|
import moment from 'moment';
|
|
|
import { qualityOptions } from '../TravelsScreen/utils/constants';
|
|
|
+import Animated, { Easing } from 'react-native-reanimated';
|
|
|
+import { useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated';
|
|
|
+import { Colors } from 'src/theme';
|
|
|
+import { useGetUniversalSearch } from '@api/search';
|
|
|
+import SearchModal from './UniversalSearch';
|
|
|
|
|
|
const localTileDir = `${FileSystem.cacheDirectory}tiles/background`;
|
|
|
const localGridDir = `${FileSystem.cacheDirectory}tiles/grid`;
|
|
|
const localVisitedDir = `${FileSystem.cacheDirectory}tiles/user_visited`;
|
|
|
const localDareDir = `${FileSystem.cacheDirectory}tiles/regions_mqp`;
|
|
|
|
|
|
-const AnimatedMarker = Animated.createAnimatedComponent(Marker);
|
|
|
+const AnimatedMarker = Animation.createAnimatedComponent(Marker);
|
|
|
|
|
|
const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
|
|
|
const [dareData, setDareData] = useState(jsonData);
|
|
@@ -92,7 +106,10 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
|
|
|
years: [],
|
|
|
id: null
|
|
|
});
|
|
|
-
|
|
|
+ const [search, setSearch] = useState('');
|
|
|
+ const [searchInput, setSearchInput] = useState('');
|
|
|
+ const { data: searchData } = useGetUniversalSearch(search);
|
|
|
+
|
|
|
useEffect(() => {
|
|
|
if (!dareData) {
|
|
|
const fetchData = async () => {
|
|
@@ -132,7 +149,13 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
|
|
|
|
|
|
const cancelTokenRef = useRef(false);
|
|
|
const currentTokenRef = useRef(0);
|
|
|
- const strokeWidthAnim = useRef(new Animated.Value(2)).current;
|
|
|
+ const strokeWidthAnim = useRef(new Animation.Value(2)).current;
|
|
|
+
|
|
|
+ const [isExpanded, setIsExpanded] = useState(false);
|
|
|
+ const [searchVisible, setSearchVisible] = useState(false);
|
|
|
+ const [index, setIndex] = useState<number>(0);
|
|
|
+ const width = useSharedValue(48);
|
|
|
+ const usableWidth = Dimensions.get('window').width - 32;
|
|
|
|
|
|
useEffect(() => {
|
|
|
if (netInfo?.isInternetReachable) {
|
|
@@ -143,14 +166,14 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
|
|
|
}, [netInfo?.isInternetReachable]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
- Animated.loop(
|
|
|
- Animated.sequence([
|
|
|
- Animated.timing(strokeWidthAnim, {
|
|
|
+ Animation.loop(
|
|
|
+ Animation.sequence([
|
|
|
+ Animation.timing(strokeWidthAnim, {
|
|
|
toValue: 3,
|
|
|
duration: 700,
|
|
|
useNativeDriver: false
|
|
|
}),
|
|
|
- Animated.timing(strokeWidthAnim, {
|
|
|
+ Animation.timing(strokeWidthAnim, {
|
|
|
toValue: 2,
|
|
|
duration: 700,
|
|
|
useNativeDriver: false
|
|
@@ -161,13 +184,31 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
|
|
|
|
|
|
useEffect(() => {
|
|
|
navigation.addListener('state', (state) => {
|
|
|
- navigation.getParent()?.setOptions({
|
|
|
+ navigation
|
|
|
+ .getParent()
|
|
|
+ ?.getParent()
|
|
|
+ ?.setOptions({
|
|
|
+ tabBarStyle: {
|
|
|
+ display:
|
|
|
+ (state.data.state.history[1] as { status?: string })?.status === 'open' ||
|
|
|
+ regionPopupVisible
|
|
|
+ ? 'none'
|
|
|
+ : 'flex',
|
|
|
+ position: 'absolute',
|
|
|
+ ...Platform.select({
|
|
|
+ android: {
|
|
|
+ height: 58
|
|
|
+ }
|
|
|
+ })
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
+ navigation
|
|
|
+ .getParent()
|
|
|
+ ?.getParent()
|
|
|
+ ?.setOptions({
|
|
|
tabBarStyle: {
|
|
|
- display:
|
|
|
- (state.data.state.history[1] as { status?: string })?.status === 'open' ||
|
|
|
- regionPopupVisible
|
|
|
- ? 'none'
|
|
|
- : 'flex',
|
|
|
+ display: regionPopupVisible ? 'none' : 'flex',
|
|
|
position: 'absolute',
|
|
|
...Platform.select({
|
|
|
android: {
|
|
@@ -176,18 +217,6 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
|
|
|
})
|
|
|
}
|
|
|
});
|
|
|
- });
|
|
|
- navigation.getParent()?.setOptions({
|
|
|
- tabBarStyle: {
|
|
|
- display: regionPopupVisible ? 'none' : 'flex',
|
|
|
- position: 'absolute',
|
|
|
- ...Platform.select({
|
|
|
- android: {
|
|
|
- height: 58
|
|
|
- }
|
|
|
- })
|
|
|
- }
|
|
|
- });
|
|
|
}, [regionPopupVisible, navigation]);
|
|
|
|
|
|
useEffect(() => {
|
|
@@ -199,16 +228,6 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
|
|
|
|
|
|
let currentLocation = await Location.getCurrentPositionAsync({});
|
|
|
setLocation(currentLocation.coords);
|
|
|
-
|
|
|
- mapRef.current?.animateToRegion(
|
|
|
- {
|
|
|
- latitude: currentLocation.coords.latitude,
|
|
|
- longitude: currentLocation.coords.longitude,
|
|
|
- latitudeDelta: 5,
|
|
|
- longitudeDelta: 5
|
|
|
- },
|
|
|
- 1000
|
|
|
- );
|
|
|
})();
|
|
|
}, []);
|
|
|
|
|
@@ -404,6 +423,81 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
+ const handleFindRegion = async (id: number, type: string) => {
|
|
|
+ cancelTokenRef.current = true;
|
|
|
+ const db = type === 'regions' ? getFirstDatabase() : getSecondDatabase();
|
|
|
+ const dataset = type === 'regions' ? regions : dareData;
|
|
|
+
|
|
|
+ const foundRegion = dataset.features.find((region: any) => region.properties.id === id);
|
|
|
+
|
|
|
+ if (foundRegion) {
|
|
|
+ setSelectedRegion({
|
|
|
+ type: 'FeatureCollection',
|
|
|
+ features: [
|
|
|
+ {
|
|
|
+ geometry: foundRegion.geometry,
|
|
|
+ properties: {
|
|
|
+ ...foundRegion.properties,
|
|
|
+ fill: 'rgba(57, 115, 172, 0.2)',
|
|
|
+ stroke: '#3973AC'
|
|
|
+ },
|
|
|
+ type: 'Feature'
|
|
|
+ }
|
|
|
+ ]
|
|
|
+ });
|
|
|
+
|
|
|
+ await getData(db, id, type, handleRegionData)
|
|
|
+ .then(() => {
|
|
|
+ setRegionPopupVisible(true);
|
|
|
+ })
|
|
|
+ .catch((error) => {
|
|
|
+ console.error('Error fetching data', error);
|
|
|
+ refreshDatabases();
|
|
|
+ });
|
|
|
+
|
|
|
+ const bounds = turf.bbox(foundRegion);
|
|
|
+ const region = calculateMapRegion(bounds);
|
|
|
+
|
|
|
+ mapRef.current?.animateToRegion(region, 1000);
|
|
|
+
|
|
|
+ if (type === 'regions') {
|
|
|
+ await mutateUserData(
|
|
|
+ { region_id: +id, token: String(token) },
|
|
|
+ {
|
|
|
+ onSuccess: (data) => {
|
|
|
+ setUserData({ type: 'nm', ...data });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ );
|
|
|
+ await mutateAsync(
|
|
|
+ { regions: JSON.stringify([id]), token: String(token) },
|
|
|
+ {
|
|
|
+ onSuccess: (data) => {
|
|
|
+ setSeries(data.series);
|
|
|
+
|
|
|
+ const allMarkers = data.items.map(processMarkerData);
|
|
|
+ setProcessedMarkers(allMarkers);
|
|
|
+ setMarkers(allMarkers);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ await mutateUserDataDare(
|
|
|
+ { dare_id: +id, token: String(token) },
|
|
|
+ {
|
|
|
+ onSuccess: (data) => {
|
|
|
+ setUserData({ type: 'dare', ...data });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ );
|
|
|
+ setProcessedMarkers([]);
|
|
|
+ setMarkers([]);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ handleClosePopup();
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
const renderMapTiles = (url: string, cacheDir: string, zIndex: number, opacity = 1) => (
|
|
|
<UrlTile
|
|
|
urlTemplate={`${url}/{z}/{x}/{y}`}
|
|
@@ -558,6 +652,35 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
|
|
|
[userData, token]
|
|
|
);
|
|
|
|
|
|
+ const handlePress = () => {
|
|
|
+ if (isExpanded) {
|
|
|
+ setIndex(0);
|
|
|
+ setSearchInput('');
|
|
|
+ }
|
|
|
+ setIsExpanded((prev) => !prev);
|
|
|
+ width.value = withTiming(isExpanded ? 48 : usableWidth, {
|
|
|
+ duration: 300,
|
|
|
+ easing: Easing.inOut(Easing.ease)
|
|
|
+ });
|
|
|
+ };
|
|
|
+
|
|
|
+ const animatedStyle = useAnimatedStyle(() => {
|
|
|
+ return {
|
|
|
+ width: width.value
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ const handleSearch = async () => {
|
|
|
+ setSearch(searchInput);
|
|
|
+ setSearchVisible(true);
|
|
|
+ };
|
|
|
+
|
|
|
+ const handleCloseModal = () => {
|
|
|
+ setSearchInput('');
|
|
|
+ setSearchVisible(false);
|
|
|
+ handlePress();
|
|
|
+ };
|
|
|
+
|
|
|
return (
|
|
|
<View style={styles.container}>
|
|
|
<ClusteredMapView
|
|
@@ -588,7 +711,7 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
|
|
|
{renderMapTiles(dareTiles, localDareDir, 2, 0.5)}
|
|
|
{location && (
|
|
|
<AnimatedMarker coordinate={location} anchor={{ x: 0.5, y: 0.5 }}>
|
|
|
- <Animated.View style={[styles.location, { borderWidth: strokeWidthAnim }]} />
|
|
|
+ <Animation.View style={[styles.location, { borderWidth: strokeWidthAnim }]} />
|
|
|
</AnimatedMarker>
|
|
|
)}
|
|
|
{markers && renderMarkers()}
|
|
@@ -638,16 +761,45 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
|
|
|
</>
|
|
|
) : (
|
|
|
<>
|
|
|
- <TouchableOpacity
|
|
|
- style={[styles.cornerButton, styles.topRightButton]}
|
|
|
- onPress={() => (navigation as any)?.openDrawer()}
|
|
|
+ {!isExpanded ? (
|
|
|
+ <TouchableOpacity
|
|
|
+ style={[styles.cornerButton, styles.topRightButton]}
|
|
|
+ onPress={() => (navigation as any)?.openDrawer()}
|
|
|
+ >
|
|
|
+ <MenuIcon />
|
|
|
+ </TouchableOpacity>
|
|
|
+ ) : null}
|
|
|
+
|
|
|
+ <Animated.View
|
|
|
+ style={[
|
|
|
+ styles.searchContainer,
|
|
|
+ styles.cornerButton,
|
|
|
+ styles.topLeftButton,
|
|
|
+ animatedStyle
|
|
|
+ ]}
|
|
|
>
|
|
|
- <MenuIcon />
|
|
|
- </TouchableOpacity>
|
|
|
-
|
|
|
- {/* <TouchableOpacity style={[styles.cornerButton, styles.topRightButton]}>
|
|
|
- <SearchIcon fill={'#0F3F4F'} />
|
|
|
- </TouchableOpacity> */}
|
|
|
+ {isExpanded ? (
|
|
|
+ <>
|
|
|
+ <TouchableOpacity onPress={handlePress} style={styles.iconButton}>
|
|
|
+ <CloseSvg fill={'#0F3F4F'} />
|
|
|
+ </TouchableOpacity>
|
|
|
+ <TextInput
|
|
|
+ style={styles.input}
|
|
|
+ placeholder="Search regions, places, nomads"
|
|
|
+ placeholderTextColor={Colors.LIGHT_GRAY}
|
|
|
+ value={searchInput}
|
|
|
+ onChangeText={(text) => setSearchInput(text)}
|
|
|
+ />
|
|
|
+ <TouchableOpacity onPress={handleSearch} style={styles.iconButton}>
|
|
|
+ <SearchIcon fill={'#0F3F4F'} />
|
|
|
+ </TouchableOpacity>
|
|
|
+ </>
|
|
|
+ ) : (
|
|
|
+ <TouchableOpacity onPress={handlePress} style={[styles.iconButton]}>
|
|
|
+ <SearchIcon fill={'#0F3F4F'} />
|
|
|
+ </TouchableOpacity>
|
|
|
+ )}
|
|
|
+ </Animated.View>
|
|
|
|
|
|
{/* <TouchableOpacity
|
|
|
style={[styles.cornerButton, styles.bottomButton, styles.bottomLeftButton]}
|
|
@@ -675,6 +827,14 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
|
|
|
updateModalState={handleModalStateChange}
|
|
|
updateNM={handleUpdateNM}
|
|
|
/>
|
|
|
+ <SearchModal
|
|
|
+ searchVisible={searchVisible}
|
|
|
+ handleCloseModal={handleCloseModal}
|
|
|
+ handleFindRegion={handleFindRegion}
|
|
|
+ index={index}
|
|
|
+ searchData={searchData}
|
|
|
+ setIndex={setIndex}
|
|
|
+ />
|
|
|
</View>
|
|
|
);
|
|
|
};
|