Prechádzať zdrojové kódy

users map + reset filter btn

Viktoriia 1 mesiac pred
rodič
commit
127faa9470

+ 5 - 0
Route.tsx

@@ -482,6 +482,11 @@ const Route = () => {
               options={regionViewScreenOptions}
             />
             <ScreenStack.Screen name={NAVIGATION_PAGES.ADD_PHOTO} component={AddPhotoScreen} />
+            <ScreenStack.Screen
+              name={NAVIGATION_PAGES.EDIT_COUNTRY_DATA}
+              component={EditCountryDataScreen}
+            />
+            <ScreenStack.Screen name={NAVIGATION_PAGES.EDIT_NM_DATA} component={EditNmDataScreen} />
           </ScreenStack.Navigator>
         )}
       </BottomTab.Screen>

+ 24 - 0
src/screens/InAppScreens/MapScreen/FilterModal/index.tsx

@@ -131,6 +131,10 @@ const FilterModal = ({
   >([]);
   const [premiumModalVisible, setPremiumModalVisible] = useState(false);
   const [toolTipVisible, setToolTipVisible] = useState<boolean>(false);
+  const [defaultYear, setDefaultYear] = useState({
+    label: moment().year().toString(),
+    value: moment().year()
+  });
 
   useEffect(() => {
     const syncSettings = async () => {
@@ -230,6 +234,7 @@ const FilterModal = ({
         .reverse();
       setAllYears(formattedYears);
       formattedYears.length && !savedFilterSettings && setSelectedYear(formattedYears[0]);
+      formattedYears.length && setDefaultYear(formattedYears[0]);
     }
   }, [data, savedFilterSettings]);
 
@@ -378,6 +383,25 @@ const FilterModal = ({
             </View>
           ) : null}
         </View>
+
+        <Button
+          children="Reset"
+          onPress={() => {
+            setTilesType({ label: 'NM regions', value: 0 });
+            setSelectedVisible({
+              label: 'visited by',
+              value: 0
+            });
+            setSelectedYear(defaultYear);
+          }}
+          variant={ButtonVariants.OPACITY}
+          containerStyles={{
+            backgroundColor: Colors.WHITE,
+            borderWidth: 1,
+            borderColor: Colors.DARK_BLUE
+          }}
+          textStyles={{ color: Colors.DARK_BLUE }}
+        />
       </View>
     );
   };

+ 627 - 185
src/screens/InAppScreens/ProfileScreen/UsersMap/index.tsx

@@ -8,7 +8,8 @@ import {
   StatusBar,
   ActivityIndicator,
   ScrollView,
-  View
+  View,
+  Text
 } from 'react-native';
 import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
 import * as Location from 'expo-location';
@@ -18,11 +19,12 @@ import Animated, {
   useAnimatedStyle,
   withTiming
 } from 'react-native-reanimated';
+import * as turf from '@turf/turf';
 
 import { styles } from './styles';
 import { API_HOST, VECTOR_MAP_HOST } from 'src/constants';
-import { CommonActions, NavigationProp } from '@react-navigation/native';
-import { AvatarWithInitials, LocationPopup } from 'src/components';
+import { CommonActions, NavigationProp, useFocusEffect } from '@react-navigation/native';
+import { AvatarWithInitials, LocationPopup, WarningModal } from 'src/components';
 import { Colors } from 'src/theme';
 
 import CloseSvg from 'assets/icons/close.svg';
@@ -47,6 +49,21 @@ import TravelsIcon from 'assets/icons/bottom-navigation/globe-solid.svg';
 import MapButton from 'src/components/MapButton';
 import ScaleBar from 'src/components/ScaleBar';
 import _ from 'lodash';
+import { SQLiteDatabase } from 'expo-sqlite';
+import { getData } from 'src/modules/map/regionData';
+import { useBottomTabBarHeight } from '@react-navigation/bottom-tabs';
+import {
+  getCountriesDatabase,
+  getFirstDatabase,
+  getSecondDatabase,
+  refreshDatabases
+} from 'src/db';
+import { fetchUserData, fetchUserDataDare, useGetListRegionsQuery } from '@api/regions';
+import { fetchCountryUserData, useGetListCountriesQuery } from '@api/countries';
+import { useRegion } from 'src/contexts/RegionContext';
+import { useGetListDareQuery } from '@api/myDARE';
+import { useConnection } from 'src/contexts/ConnectionContext';
+import RegionPopup from 'src/components/RegionPopup';
 const defaultUserAvatar = require('assets/icon-user-share-location-solid.png');
 
 const generateFilter = (ids: number[]) => {
@@ -134,6 +151,31 @@ let dare = {
   maxzoom: 16
 };
 
+let selected_region = {
+  id: 'selected_region',
+  type: 'fill',
+  source: 'regions',
+  'source-layer': 'regions',
+  style: {
+    fillColor: 'rgba(57, 115, 172, 0.3)'
+  },
+  maxzoom: 12
+};
+
+let selected_region_outline = {
+  id: 'selected_region_outline',
+  type: 'line',
+  source: 'regions',
+  'source-layer': 'regions',
+  style: {
+    lineColor: '#ED9334',
+    lineTranslate: [0, 0],
+    lineTranslateAnchor: 'map',
+    lineWidth: ['interpolate', ['linear'], ['zoom'], 0, 2, 4, 3, 5, 4, 12, 5]
+  },
+  maxzoom: 12
+};
+
 type Props = {
   navigation: NavigationProp<any>;
   route: any;
@@ -143,6 +185,7 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
   const token = storage.get('token', StoreType.STRING) as string;
   const userId = route.params?.userId;
   const data = route.params?.data;
+  const tabBarHeight = useBottomTabBarHeight();
 
   const [regionsVisitedFilter, setRegionsVisitedFilter] = useState(generateFilter([]));
   const [countriesVisitedFilter, setCountriesVisitedFilter] = useState(generateFilter([]));
@@ -205,6 +248,28 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
     type === 'dare' && !!userId
   );
 
+  const [isConnected, setIsConnected] = useState<boolean>(true);
+  const netInfo = useConnection();
+
+  const { mutateAsync: mutateUserData } = fetchUserData();
+  const { mutateAsync: mutateUserDataDare } = fetchUserDataDare();
+  const { mutateAsync: mutateCountriesData } = fetchCountryUserData();
+  const { data: regionsList } = useGetListRegionsQuery(isConnected);
+  const { data: countriesList } = useGetListCountriesQuery(isConnected);
+  const { data: dareList } = useGetListDareQuery(isConnected);
+
+  const { handleUpdateNM, handleUpdateDare, handleUpdateSlow, userData, setUserData } = useRegion();
+
+  const [selectedRegion, setSelectedRegion] = useState<number | null>(null);
+  const [db1, setDb1] = useState<SQLiteDatabase | null>(null);
+  const [db2, setDb2] = useState<SQLiteDatabase | null>(null);
+  const [db3, setDb3] = useState<SQLiteDatabase | null>(null);
+  const [regionData, setRegionData] = useState<any | null>(null);
+  const [userAvatars, setUserAvatars] = useState<string[]>([]);
+  const [isWarningModalVisible, setIsWarningModalVisible] = useState<boolean>(false);
+
+  const [regionPopupVisible, setRegionPopupVisible] = useState<boolean | null>(false);
+
   const cameraController = {
     flyTo: useCallback((coordinates: number[], duration: number = 1000) => {
       isAnimatingRef.current = true;
@@ -253,9 +318,69 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
           (config.animationDuration ?? 1000) + 100
         );
       }
+    }, []),
+
+    fitBounds: useCallback((ne: number[], sw: number[], padding: number[], duration: number) => {
+      isAnimatingRef.current = true;
+
+      if (animationTimeoutRef.current) {
+        clearTimeout(animationTimeoutRef.current);
+      }
+
+      if (Platform.OS === 'android') {
+        setRenderCamera(true);
+
+        requestAnimationFrame(() => {
+          cameraRef.current?.fitBounds(ne, sw, padding, duration);
+        });
+
+        animationTimeoutRef.current = setTimeout(() => {
+          isAnimatingRef.current = false;
+          setRenderCamera(false);
+        }, duration + 200);
+      } else {
+        cameraRef.current?.fitBounds(ne, sw, padding, duration);
+
+        animationTimeoutRef.current = setTimeout(() => {
+          isAnimatingRef.current = false;
+        }, duration + 100);
+      }
     }, [])
   };
 
+  useFocusEffect(
+    useCallback(() => {
+      navigation.getParent()?.setOptions({
+        tabBarStyle: {
+          display: regionPopupVisible ? 'none' : 'flex',
+          position: 'absolute'
+        }
+      });
+    }, [regionPopupVisible, navigation])
+  );
+
+  useEffect(() => {
+    if (netInfo && netInfo.isConnected !== null) {
+      setIsConnected(netInfo.isConnected);
+    }
+  }, [netInfo]);
+
+  useEffect(() => {
+    const loadDatabases = async () => {
+      const firstDb = await getFirstDatabase();
+      const secondDb = await getSecondDatabase();
+      const countriesDb = await getCountriesDatabase();
+
+      setDb1(firstDb);
+      setDb2(secondDb);
+      setDb3(countriesDb);
+    };
+
+    if (!db1 || !db2 || !db3) {
+      loadDatabases();
+    }
+  }, [db1, db2, db3]);
+
   useEffect(() => {
     if (visitedRegionIds) {
       setRegionsVisitedFilter(generateFilter(visitedRegionIds.ids));
@@ -394,25 +519,230 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
     handlePress();
   };
 
-  const handleFindRegion = (id: number, type: string) => {
-    navigation.dispatch(
-      CommonActions.reset({
-        index: 1,
-        routes: [
-          {
-            name: NAVIGATION_PAGES.IN_APP_MAP_TAB,
-            state: {
-              routes: [
+  const handleRegionData = async (regionData: any, avatars: string[]) => {
+    if (!regionData) {
+      await refreshDatabases();
+    }
+    setRegionData(regionData);
+    setUserAvatars(avatars);
+  };
+
+  const handleClosePopup = async () => {
+    setSelectedRegion(null);
+    setRegionPopupVisible(false);
+    setRegionData(null);
+  };
+
+  const handleOpenEditModal = () => {
+    navigation.navigate(NAVIGATION_PAGES.EDIT_NM_DATA, { regionId: regionData?.id });
+  };
+
+  const handleOpenEditSlowModal = () => {};
+
+  const handleFindRegion = async (id: number, type: string) => {
+    setType(type === 'places' ? 'dare' : type);
+    if (!db1 || !db2 || !db3) {
+      return;
+    }
+    const db = type === 'regions' ? db1 : type === 'countries' ? db3 : db2;
+
+    if (id) {
+      setSelectedRegion(id);
+
+      await getData(db, id, type, handleRegionData)
+        .then(() => {
+          setRegionPopupVisible(true);
+        })
+        .catch((error) => {
+          console.error('Error fetching data', error);
+          refreshDatabases();
+        });
+
+      if (type === 'regions') {
+        token
+          ? await mutateUserData(
+              { region_id: id, token: String(token) },
+              {
+                onSuccess: (data) => {
+                  setUserData({ type: 'nm', id, ...data });
+                }
+              }
+            )
+          : setUserData({ type: 'nm', id });
+
+        if (regionsList && regionsList.data) {
+          const region = regionsList.data.find((region) => region.id === +id);
+          if (region) {
+            const bounds = turf.bbox(region.bbox);
+            cameraController.fitBounds(
+              [bounds[2], bounds[3]],
+              [bounds[0], bounds[1]],
+              [10, 10, 50, 10],
+              1000
+            );
+          }
+        }
+      } else if (type === 'countries') {
+        token
+          ? await mutateCountriesData(
+              { id, token },
+              {
+                onSuccess: (data) => {
+                  setUserData({ type: 'countries', id, ...data.data });
+                }
+              }
+            )
+          : setUserData({ type: 'countries', id });
+
+        if (countriesList && countriesList.data) {
+          const region = countriesList.data.find((region) => region.id === +id);
+          if (region) {
+            const bounds = turf.bbox(region.bbox);
+            cameraController.fitBounds(
+              [bounds[2], bounds[3]],
+              [bounds[0], bounds[1]],
+              [10, 10, 50, 10],
+              1000
+            );
+          }
+        }
+      } else {
+        token
+          ? await mutateUserDataDare(
+              { dare_id: +id, token: String(token) },
+              {
+                onSuccess: (data) => {
+                  setUserData({ type: 'dare', id: +id, ...data });
+                }
+              }
+            )
+          : setUserData({ type: 'dare', id: +id });
+
+        if (dareList && dareList.data) {
+          const region = dareList.data.find((region) => region.id === +id);
+          if (region) {
+            const bounds = turf.bbox(region.bbox);
+            cameraController.fitBounds(
+              [bounds[2], bounds[3]],
+              [bounds[0], bounds[1]],
+              [10, 10, 50, 10],
+              1000
+            );
+          }
+        }
+      }
+    } else {
+      handleClosePopup();
+    }
+  };
+
+  const onMapPress = async (event: any) => {
+    if (!mapRef.current) return;
+    if (type === 'blank') return;
+    try {
+      const { screenPointX, screenPointY } = event.properties;
+
+      const { features } = await mapRef.current.queryRenderedFeaturesAtPoint(
+        [screenPointX, screenPointY],
+        undefined,
+        ['regions', 'countries', 'dare']
+      );
+
+      if (features?.length) {
+        const region = features[0];
+        if (selectedRegion === region.properties?.id) return;
+
+        let db = type === 'regions' ? db1 : type === 'countries' ? db3 : db2;
+        let tableName = type === 'dare' ? 'places' : type;
+        let foundRegion = region.properties?.id;
+
+        setSelectedRegion(region.properties?.id);
+
+        await getData(db, foundRegion, tableName, handleRegionData)
+          .then(() => {
+            setRegionPopupVisible(true);
+          })
+          .catch((error) => {
+            console.error('Error fetching data', error);
+            refreshDatabases();
+          });
+
+        if (tableName === 'regions') {
+          token
+            ? await mutateUserData(
+                { region_id: +foundRegion, token: String(token) },
+                {
+                  onSuccess: (data) => {
+                    setUserData({ type: 'nm', id: +foundRegion, ...data });
+                  }
+                }
+              )
+            : setUserData({ type: 'nm', id: +foundRegion });
+          if (regionsList && regionsList.data) {
+            const region = regionsList.data.find((region) => region.id === +foundRegion);
+            if (region) {
+              const bounds = turf.bbox(region.bbox);
+              cameraController.fitBounds(
+                [bounds[2], bounds[3]],
+                [bounds[0], bounds[1]],
+                [10, 10, 50, 10],
+                1000
+              );
+            }
+          }
+        } else if (tableName === 'countries') {
+          token
+            ? await mutateCountriesData(
+                { id: +foundRegion, token },
+                {
+                  onSuccess: (data) => {
+                    setUserData({ type: 'countries', id: +foundRegion, ...data.data });
+                  }
+                }
+              )
+            : setUserData({ type: 'countries', id: +foundRegion });
+          if (countriesList && countriesList.data) {
+            const region = countriesList.data.find((region) => region.id === +foundRegion);
+            if (region) {
+              const bounds = turf.bbox(region.bbox);
+              cameraController.fitBounds(
+                [bounds[2], bounds[3]],
+                [bounds[0], bounds[1]],
+                [10, 10, 50, 10],
+                1000
+              );
+            }
+          }
+        } else {
+          token
+            ? await mutateUserDataDare(
+                { dare_id: +foundRegion, token: String(token) },
                 {
-                  name: NAVIGATION_PAGES.MAP_TAB,
-                  params: { id, type }
+                  onSuccess: (data) => {
+                    setUserData({ type: 'dare', id: +foundRegion, ...data });
+                  }
                 }
-              ]
+              )
+            : setUserData({ type: 'dare', id: +foundRegion });
+          if (dareList && dareList.data) {
+            const region = dareList.data.find((region) => region.id === +foundRegion);
+            if (region) {
+              const bounds = turf.bbox(region.bbox);
+              cameraController.fitBounds(
+                [bounds[2], bounds[3]],
+                [bounds[0], bounds[1]],
+                [10, 10, 50, 10],
+                1000
+              );
             }
           }
-        ]
-      })
-    );
+        }
+      } else {
+        handleClosePopup();
+      }
+    } catch (error) {
+      console.error('Error onMapPress features:', error);
+    }
   };
 
   const locationFeature: GeoJSON.Feature<GeoJSON.Point> = {
@@ -434,102 +764,141 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
         mapStyle={VECTOR_MAP_HOST + '/nomadmania-maps2025.json'}
         rotateEnabled={false}
         attributionEnabled={false}
+        onPress={onMapPress}
         onRegionDidChange={() => {
           hideTimer.current = setTimeout(() => {
             setIsZooming(false);
           }, 2000);
         }}
-        // onRegionIsChanging={handleMapChange}
         onRegionWillChange={_.debounce(handleMapChange, 200)}
       >
-        {type === 'regions' && (
+        <>
+          <MapLibreRN.LineLayer
+            id="nm-regions-line-layer"
+            sourceID={regions.source}
+            sourceLayerID={regions['source-layer']}
+            filter={regions.filter as any}
+            maxZoomLevel={regions.maxzoom}
+            style={{
+              lineColor: 'rgba(14, 80, 109, 1)',
+              lineWidth: ['interpolate', ['linear'], ['zoom'], 0, 0.2, 4, 1, 5, 1.5, 12, 3],
+              lineWidthTransition: { duration: 300, delay: 0 },
+              visibility: type === 'regions' ? 'visible' : 'none'
+            }}
+            belowLayerID="waterway-name"
+          />
+          <MapLibreRN.FillLayer
+            id={regions.id}
+            sourceID={regions.source}
+            sourceLayerID={regions['source-layer']}
+            filter={regions.filter as any}
+            style={{
+              ...regions.style,
+              visibility: type === 'regions' ? 'visible' : 'none'
+            }}
+            maxZoomLevel={regions.maxzoom}
+            belowLayerID={regions_visited.id}
+          />
+          <MapLibreRN.FillLayer
+            id={regions_visited.id}
+            sourceID={regions_visited.source}
+            sourceLayerID={regions_visited['source-layer']}
+            filter={regionsVisitedFilter as any}
+            style={{
+              ...regions_visited.style,
+              visibility: type === 'regions' ? 'visible' : 'none'
+            }}
+            maxZoomLevel={regions_visited.maxzoom}
+            belowLayerID="waterway-name"
+          />
+        </>
+
+        <>
+          <MapLibreRN.LineLayer
+            id="countries-line-layer"
+            sourceID={countries.source}
+            sourceLayerID={countries['source-layer']}
+            filter={countries.filter as any}
+            maxZoomLevel={countries.maxzoom}
+            style={{
+              lineColor: 'rgba(14, 80, 109, 1)',
+              lineWidth: ['interpolate', ['linear'], ['zoom'], 0, 0.2, 4, 1, 5, 1.5, 12, 3],
+              lineWidthTransition: { duration: 300, delay: 0 },
+              visibility: type === 'countries' ? 'visible' : 'none'
+            }}
+            belowLayerID="waterway-name"
+          />
+          <MapLibreRN.FillLayer
+            id={countries.id}
+            sourceID={countries.source}
+            sourceLayerID={countries['source-layer']}
+            filter={countries.filter as any}
+            style={{
+              ...countries.style,
+              visibility: type === 'countries' ? 'visible' : 'none'
+            }}
+            maxZoomLevel={countries.maxzoom}
+            belowLayerID={countries_visited.id}
+          />
+          <MapLibreRN.FillLayer
+            id={countries_visited.id}
+            sourceID={countries_visited.source}
+            sourceLayerID={countries_visited['source-layer']}
+            filter={countriesVisitedFilter as any}
+            style={{
+              ...countries_visited.style,
+              visibility: type === 'countries' ? 'visible' : 'none'
+            }}
+            maxZoomLevel={countries_visited.maxzoom}
+            belowLayerID="waterway-name"
+          />
+        </>
+
+        <>
+          <MapLibreRN.FillLayer
+            id={dare.id}
+            sourceID={dare.source}
+            sourceLayerID={dare['source-layer']}
+            filter={dare.filter as any}
+            style={{
+              ...dare.style,
+              visibility: type === 'dare' ? 'visible' : 'none'
+            }}
+            maxZoomLevel={dare.maxzoom}
+            belowLayerID={dare_visited.id}
+          />
+          <MapLibreRN.FillLayer
+            id={dare_visited.id}
+            sourceID={dare_visited.source}
+            sourceLayerID={dare_visited['source-layer']}
+            filter={dareVisitedFilter as any}
+            style={{
+              ...dare_visited.style,
+              visibility: type === 'dare' ? 'visible' : 'none'
+            }}
+            maxZoomLevel={dare_visited.maxzoom}
+            belowLayerID="waterway-name"
+          />
+        </>
+
+        {selectedRegion && type && (
           <>
-            <MapLibreRN.LineLayer
-              id="nm-regions-line-layer"
-              sourceID={regions.source}
-              sourceLayerID={regions['source-layer']}
-              filter={regions.filter as any}
-              maxZoomLevel={regions.maxzoom}
-              style={{
-                lineColor: 'rgba(14, 80, 109, 1)',
-                lineWidth: ['interpolate', ['linear'], ['zoom'], 0, 0.2, 4, 1, 5, 1.5, 12, 3],
-                lineWidthTransition: { duration: 300, delay: 0 }
-              }}
-              belowLayerID="waterway-name"
-            />
-            <MapLibreRN.FillLayer
-              id={regions.id}
-              sourceID={regions.source}
-              sourceLayerID={regions['source-layer']}
-              filter={regions.filter as any}
-              style={regions.style}
-              maxZoomLevel={regions.maxzoom}
-              belowLayerID={regions_visited.id}
-            />
             <MapLibreRN.FillLayer
-              id={regions_visited.id}
-              sourceID={regions_visited.source}
-              sourceLayerID={regions_visited['source-layer']}
-              filter={regionsVisitedFilter as any}
-              style={regions_visited.style}
-              maxZoomLevel={regions_visited.maxzoom}
+              id={selected_region.id}
+              sourceID={type}
+              sourceLayerID={type}
+              filter={['==', 'id', selectedRegion]}
+              style={selected_region.style}
+              maxZoomLevel={selected_region.maxzoom}
               belowLayerID="waterway-name"
             />
-          </>
-        )}
-        {type === 'countries' && (
-          <>
             <MapLibreRN.LineLayer
-              id="countries-line-layer"
-              sourceID={countries.source}
-              sourceLayerID={countries['source-layer']}
-              filter={countries.filter as any}
-              maxZoomLevel={countries.maxzoom}
-              style={{
-                lineColor: 'rgba(14, 80, 109, 1)',
-                lineWidth: ['interpolate', ['linear'], ['zoom'], 0, 0.2, 4, 1, 5, 1.5, 12, 3],
-                lineWidthTransition: { duration: 300, delay: 0 }
-              }}
-              belowLayerID="waterway-name"
-            />
-            <MapLibreRN.FillLayer
-              id={countries.id}
-              sourceID={countries.source}
-              sourceLayerID={countries['source-layer']}
-              filter={countries.filter as any}
-              style={countries.style}
-              maxZoomLevel={countries.maxzoom}
-              belowLayerID={countries_visited.id}
-            />
-            <MapLibreRN.FillLayer
-              id={countries_visited.id}
-              sourceID={countries_visited.source}
-              sourceLayerID={countries_visited['source-layer']}
-              filter={countriesVisitedFilter as any}
-              style={countries_visited.style}
-              maxZoomLevel={countries_visited.maxzoom}
-              belowLayerID="waterway-name"
-            />
-          </>
-        )}
-        {type === 'dare' && (
-          <>
-            <MapLibreRN.FillLayer
-              id={dare.id}
-              sourceID={dare.source}
-              sourceLayerID={dare['source-layer']}
-              filter={dare.filter as any}
-              style={dare.style}
-              maxZoomLevel={dare.maxzoom}
-              belowLayerID={dare_visited.id}
-            />
-            <MapLibreRN.FillLayer
-              id={dare_visited.id}
-              sourceID={dare_visited.source}
-              sourceLayerID={dare_visited['source-layer']}
-              filter={dareVisitedFilter as any}
-              style={dare_visited.style}
-              maxZoomLevel={dare_visited.maxzoom}
+              id={selected_region_outline.id}
+              sourceID={type}
+              sourceLayerID={type}
+              filter={['==', 'id', selectedRegion]}
+              style={selected_region_outline.style as any}
+              maxZoomLevel={selected_region_outline.maxzoom}
               belowLayerID="waterway-name"
             />
           </>
@@ -577,94 +946,161 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
         <ScaleBar zoom={zoom} latitude={center[1]} isVisible={isZooming} bottom={80} />
       ) : null}
 
-      {!isExpanded ? (
-        <TouchableOpacity
-          style={[styles.cornerButton, styles.topRightButton]}
-          onPress={handleGoBack}
-        >
-          {data.user_data.avatar ? (
-            <Image
-              style={styles.avatar}
-              source={{ uri: API_HOST + '/img/avatars/' + data.user_data.avatar }}
-            />
-          ) : (
-            <AvatarWithInitials
-              text={`${data.user_data.first_name[0] ?? ''}${data.user_data.last_name[0] ?? ''}`}
-              flag={API_HOST + '/img/flags_new/' + data.user_data.flag1}
-              size={48}
-              borderColor={Colors.WHITE}
-            />
-          )}
-        </TouchableOpacity>
-      ) : null}
+      {regionPopupVisible && regionData ? (
+        <>
+          <TouchableOpacity
+            style={[styles.cornerButton, styles.topLeftButton, styles.closeLeftButton]}
+            onPress={handleClosePopup}
+          >
+            <CloseSvg fill="white" width={13} height={13} />
+            <Text style={styles.textClose}>Close</Text>
+          </TouchableOpacity>
 
-      <View style={styles.tabs}>
-        <ScrollView
-          horizontal
-          showsHorizontalScrollIndicator={false}
-          contentContainerStyle={{ marginLeft: 12, gap: 12, flexDirection: 'row' }}
-        >
-          <MapButton
-            onPress={() => {
-              setIsFilterVisible('regions');
+          <TouchableOpacity
+            onPress={handleGetLocation}
+            style={[
+              styles.cornerButton,
+              styles.topRightButton,
+              styles.bottomButton,
+              { bottom: tabBarHeight + 20 }
+            ]}
+          >
+            {isLocationLoading ? (
+              <ActivityIndicator size="small" color={Colors.DARK_BLUE} />
+            ) : (
+              <LocationIcon />
+            )}
+          </TouchableOpacity>
+
+          <RegionPopup
+            region={regionData}
+            userAvatars={userAvatars}
+            userData={userData}
+            openEditModal={handleOpenEditModal}
+            updateNM={(id, first, last, visits, quality) => {
+              if (!token) {
+                setIsWarningModalVisible(true);
+                return;
+              }
+              handleUpdateNM(id, first, last, visits, quality);
             }}
-            icon={TravelsIcon}
-            text={
-              tilesType.value === 0 || tilesType.value === 1
-                ? token
-                  ? `${tilesType.value === 0 ? 'NM' : 'UN'} ${regionsFilter.visitedLabel} ${regionsFilter.year}`
-                  : `${tilesType.value === 0 ? 'NM' : 'UN'}`
-                : tilesType.value === 2
-                  ? 'DARE'
-                  : 'Travels'
-            }
-            active={true}
+            updateDare={(id, visits) => {
+              if (!token) {
+                setIsWarningModalVisible(true);
+                return;
+              }
+              handleUpdateDare(id, visits);
+            }}
+            disabled={!token || !isConnected}
+            updateSlow={(id, v, s11, s31, s101) => {
+              if (!token) {
+                setIsWarningModalVisible(true);
+                return;
+              }
+              handleUpdateSlow(id, v, s11, s31, s101);
+            }}
+            openEditSlowModal={handleOpenEditSlowModal}
           />
-        </ScrollView>
-      </View>
-
-      <TouchableOpacity
-        onPress={handleGetLocation}
-        style={[styles.cornerButton, styles.bottomButton, styles.bottomRightButton]}
-      >
-        {isLocationLoading ? (
-          <ActivityIndicator size="small" color={Colors.DARK_BLUE} />
-        ) : (
-          <LocationIcon />
-        )}
-      </TouchableOpacity>
-      <Animated.View
-        style={[
-          styles.searchContainer,
-          styles.cornerButton,
-          styles.topLeftButton,
-          animatedStyle,
-          { padding: 5 }
-        ]}
-      >
-        {isExpanded ? (
-          <>
-            <TouchableOpacity onPress={handlePress} style={styles.iconButton}>
-              <CloseSvg fill={'#0F3F4F'} />
+        </>
+      ) : (
+        <>
+          {!isExpanded ? (
+            <TouchableOpacity
+              style={[styles.cornerButton, styles.topRightButton]}
+              onPress={handleGoBack}
+            >
+              {data.user_data.avatar ? (
+                <Image
+                  style={styles.avatar}
+                  source={{ uri: API_HOST + '/img/avatars/' + data.user_data.avatar }}
+                />
+              ) : (
+                <AvatarWithInitials
+                  text={`${data.user_data.first_name[0] ?? ''}${data.user_data.last_name[0] ?? ''}`}
+                  flag={API_HOST + '/img/flags_new/' + data.user_data.flag1}
+                  size={48}
+                  borderColor={Colors.WHITE}
+                />
+              )}
             </TouchableOpacity>
-            <TextInput
-              style={styles.input}
-              placeholder="Search regions, places, nomads"
-              placeholderTextColor={Colors.LIGHT_GRAY}
-              value={searchInput}
-              onChangeText={(text) => setSearchInput(text)}
-              onSubmitEditing={handleSearch}
-            />
-            <TouchableOpacity onPress={handleSearch} style={styles.iconButton}>
-              <SearchIcon fill={'#0F3F4F'} />
-            </TouchableOpacity>
-          </>
-        ) : (
-          <TouchableOpacity onPress={handlePress} style={[styles.iconButton]}>
-            <SearchIcon fill={'#0F3F4F'} />
+          ) : null}
+
+          <Animated.View
+            style={[
+              styles.searchContainer,
+              styles.cornerButton,
+              styles.topLeftButton,
+              animatedStyle,
+              { padding: 5 }
+            ]}
+          >
+            {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)}
+                  onSubmitEditing={handleSearch}
+                />
+                <TouchableOpacity onPress={handleSearch} style={styles.iconButton}>
+                  <SearchIcon fill={'#0F3F4F'} />
+                </TouchableOpacity>
+              </>
+            ) : (
+              <TouchableOpacity onPress={handlePress} style={[styles.iconButton]}>
+                <SearchIcon fill={'#0F3F4F'} />
+              </TouchableOpacity>
+            )}
+          </Animated.View>
+
+          <View style={[styles.tabs, { bottom: tabBarHeight + 20 }]}>
+            <ScrollView
+              horizontal
+              showsHorizontalScrollIndicator={false}
+              contentContainerStyle={{ marginLeft: 12, gap: 12, flexDirection: 'row' }}
+            >
+              <MapButton
+                onPress={() => {
+                  setIsFilterVisible('regions');
+                }}
+                icon={TravelsIcon}
+                text={
+                  tilesType.value === 0 || tilesType.value === 1
+                    ? token
+                      ? `${tilesType.value === 0 ? 'NM' : 'UN'} ${regionsFilter.visitedLabel} ${regionsFilter.year}`
+                      : `${tilesType.value === 0 ? 'NM' : 'UN'}`
+                    : tilesType.value === 2
+                      ? 'DARE'
+                      : 'Travels'
+                }
+                active={true}
+              />
+            </ScrollView>
+          </View>
+
+          <TouchableOpacity
+            onPress={handleGetLocation}
+            style={[
+              styles.cornerButton,
+              styles.bottomButton,
+              styles.bottomRightButton,
+              { bottom: tabBarHeight + 20 }
+            ]}
+          >
+            {isLocationLoading ? (
+              <ActivityIndicator size="small" color={Colors.DARK_BLUE} />
+            ) : (
+              <LocationIcon />
+            )}
           </TouchableOpacity>
-        )}
-      </Animated.View>
+        </>
+      )}
+
       <FilterModal
         isFilterVisible={isFilterVisible}
         setIsFilterVisible={setIsFilterVisible}
@@ -708,6 +1144,12 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
         setIndex={setIndex}
         token={token}
       />
+
+      <WarningModal
+        type={'unauthorized'}
+        isVisible={isWarningModalVisible}
+        onClose={() => setIsWarningModalVisible(false)}
+      />
     </SafeAreaView>
   );
 };