Browse Source

Merge branch 'dev' of https://git.nomadmania.travel/Viktoriia/nomadmania-app into buttons-relocation

Viktoriia 11 months ago
parent
commit
e7fe39510f

+ 2 - 2
app.config.ts

@@ -20,7 +20,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
   owner: 'nomadmaniaou',
   scheme: 'nm',
   // Should be updated after every production release (deploy to AppStore/PlayMarket)
-  version: '2.0.5',
+  version: '2.0.7',
   // Should be updated after every dependency change
   runtimeVersion: '1.5',
   orientation: 'portrait',
@@ -86,7 +86,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
       'INTERNET',
       'CAMERA'
     ],
-    versionCode: 56 // next version submitted to Google Play needs to be higher than that 2.0.5
+    versionCode: 58 // next version submitted to Google Play needs to be higher than that 2.0.7
   },
   plugins: [
     [

+ 1 - 0
package.json

@@ -59,6 +59,7 @@
     "react-native-mmkv": "^2.11.0",
     "react-native-modal": "^13.0.1",
     "react-native-pager-view": "6.2.0",
+    "react-native-paper": "^5.12.3",
     "react-native-progress": "^5.0.1",
     "react-native-reanimated": "~3.3.0",
     "react-native-reanimated-carousel": "^3.5.1",

+ 1 - 0
src/modules/api/series/queries/index.ts

@@ -8,3 +8,4 @@ export * from './use-post-get-series-ranking';
 export * from './use-post-get-data-from-point';
 export * from './use-post-get-suggestion-data';
 export * from './use-post-submit-suggestion';
+export * from './use-post-get-list';

+ 17 - 0
src/modules/api/series/queries/use-post-get-list.tsx

@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { seriesQueryKeys } from '../series-query-keys';
+import { seriesApi, type PostGetSeriesList } from '../series-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const useGetListQuery = (enabled: boolean) => {
+  return useQuery<PostGetSeriesList, BaseAxiosError>({
+    queryKey: seriesQueryKeys.getList(),
+    queryFn: async () => {
+      const response = await seriesApi.getList();
+      return response.data;
+    },
+    enabled
+  });
+};

+ 2 - 1
src/modules/api/series/series-api.tsx

@@ -180,5 +180,6 @@ export const seriesApi = {
     request.postForm<PostGetDataFromPoint>(API.GET_DATA_FROM_POINT, { token, lat, lng }),
   getSuggestionData: () => request.postForm<PostGetSuggestionData>(API.GET_SUGGESTION_DATA),
   submitSuggestion: (data: SubmitSuggestionTypes) =>
-    request.postForm<SubmitSuggestionReturn>(API.SUBMIT_SUGGESTION, data)
+    request.postForm<SubmitSuggestionReturn>(API.SUBMIT_SUGGESTION, data),
+  getList: () => request.postForm<PostGetSeriesList>(API.GET_SERIES_LIST)
 };

+ 2 - 1
src/modules/api/series/series-query-keys.tsx

@@ -8,5 +8,6 @@ export const seriesQueryKeys = {
   getSeriesRanking: (id: number, page: number, page_size: number) => ['getSeriesRanking', {id, page, page_size}] as const,
   getDataFromPoint: (token: string, lat: number, lng: number) => ['getDataFromPoint', {token, lat, lng}] as const,
   getSuggestionData: () => ['getSuggestionData'] as const,
-  submitSuggestion: () => ['submitSuggestion'] as const
+  submitSuggestion: () => ['submitSuggestion'] as const,
+  getList: () => ['getList'] as const,
 };

+ 1 - 0
src/modules/api/user/queries/index.ts

@@ -5,3 +5,4 @@ export * from './use-post-get-profile-info-public';
 export * from './use-post-get-profile-regions';
 export * from './use-post-get-profile-data';
 export * from './use-post-get-profile-updates';
+export * from './use-post-get-map-years';

+ 17 - 0
src/modules/api/user/queries/use-post-get-map-years.tsx

@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { userQueryKeys } from '../user-query-keys';
+import { type PostGetMapYearsReturn, userApi } from '../user-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const usePostGetMapYearsQuery = (userId: number, enabled: boolean) => {
+  return useQuery<PostGetMapYearsReturn, BaseAxiosError>({
+    queryKey: userQueryKeys.getMapYears(userId),
+    queryFn: async () => {
+      const response = await userApi.getMapYears(userId);
+      return response.data;
+    },
+    enabled
+  });
+};

+ 15 - 1
src/modules/api/user/user-api.tsx

@@ -281,6 +281,18 @@ export interface PostGetProfileUpdatesReturn extends ResponseType {
   };
 }
 
+export interface PostGetMapYearsReturn extends ResponseType {
+  data: {
+    map_years: number[];
+    map_count_nm: { [key: string]: number };
+    map_count_un: { [key: string]: number };
+    map_count_in_nm: { [key: string]: number };
+    map_count_in_un: { [key: string]: number };
+    max_nm: number;
+    max_un: number;
+  };
+}
+
 export const userApi = {
   getProfileData: (token: string) =>
     request.postForm<PostGetProfileData>(API.GET_USER_SETTINGS_DATA, { token }),
@@ -312,5 +324,7 @@ export const userApi = {
     request.postForm<PostGetProfileUpdatesReturn>(API.GET_PROFILE_UPDATES, {
       token,
       profile_id
-    })
+    }),
+  getMapYears: (profile_id: number) =>
+    request.postForm<PostGetMapYearsReturn>(API.GET_MAP_YEARS, { profile_id })
 };

+ 2 - 1
src/modules/api/user/user-query-keys.tsx

@@ -5,5 +5,6 @@ export const userQueryKeys = {
   getProfileInfoPublic: () => ['getProfileInfoPublic'] as const,
   getProfileRegions: (uid: number, type: string) => ['getProfileRegions', uid, type] as const,
   getProfileInfoData: (userId: number) => ['getProfileInfoData', userId] as const,
-  getProfileUpdates: (userId: number) => ['getProfileUpdates', userId] as const
+  getProfileUpdates: (userId: number) => ['getProfileUpdates', userId] as const,
+  getMapYears: (userId: number) => ['getMapYears', userId] as const
 };

+ 313 - 60
src/screens/InAppScreens/MapScreen/FilterModal/index.tsx

@@ -1,13 +1,18 @@
-import React from 'react';
-import { View, Text, TouchableOpacity } from 'react-native';
-import Modal from 'react-native-modal';
-import CloseSvg from 'assets/icons/close.svg';
-import FilterIcon from 'assets/icons/filter.svg';
+import React, { useEffect, useState } from 'react';
+import { View, Text, TouchableOpacity, Image, Switch } from 'react-native';
+import ReactModal from 'react-native-modal';
 import { Colors } from 'src/theme';
 import { ModalStyles } from '../../TravellersScreen/Components/styles';
-import { Dropdown } from 'react-native-searchable-dropdown-kj';
+import { Dropdown, MultiSelect } from 'react-native-searchable-dropdown-kj';
 import { Button } from 'src/components';
 import { ButtonVariants } from 'src/types/components';
+import { styles } from './styles';
+import { TabBar, TabView } from 'react-native-tab-view';
+import { usePostGetMapYearsQuery } from '@api/user';
+import { API_HOST, FASTEST_MAP_HOST } from 'src/constants';
+import CheckSvg from 'assets/icons/mark.svg';
+import { useGetListQuery } from '@api/series';
+import { RadioButton } from 'react-native-paper';
 
 const FilterModal = ({
   isFilterVisible,
@@ -16,7 +21,11 @@ const FilterModal = ({
   tilesType,
   setTilesType,
   type,
-  setType
+  setType,
+  userId,
+  setVisitedTiles,
+  setSeriesFilter,
+  isPublicView
 }: {
   isFilterVisible: boolean;
   setIsFilterVisible: (isVisible: boolean) => void;
@@ -25,28 +34,94 @@ const FilterModal = ({
   setTilesType: (item: any) => void;
   type: number;
   setType: (type: any) => void;
+  userId: number;
+  setVisitedTiles: (tiles: string) => void;
+  setSeriesFilter?: (filter: any) => void;
+  isPublicView: boolean;
 }) => {
-  return (
-    <Modal isVisible={isFilterVisible}>
-      <View style={{ backgroundColor: 'white', borderRadius: 15 }}>
-        <View style={{ marginHorizontal: '5%', marginTop: '5%', marginBottom: '10%' }}>
-          <View style={{ alignSelf: 'flex-end' }}>
-            <TouchableOpacity
-              onPress={() => {
-                type === 0
-                  ? setTilesType({ label: 'NM regions', value: 0 })
-                  : setTilesType({ label: 'UN countries', value: 1 });
-                setIsFilterVisible(!isFilterVisible);
-              }}
-            >
-              <CloseSvg fill={Colors.LIGHT_GRAY} />
-            </TouchableOpacity>
-          </View>
-          <View style={{ display: 'flex', alignItems: 'center' }}>
-            <Text style={{ color: Colors.DARK_BLUE, fontSize: 20, fontWeight: '700' }}>
-              Map filter show visited
-            </Text>
-            <View style={[ModalStyles.ageAndRankingWrapper, { marginTop: 24 }]}>
+  const [index, setIndex] = useState(0);
+  const [selectedYear, setSelectedYear] = useState<{ label: string; value: number } | null>(null);
+  const [allYears, setAllYears] = useState<{ label: string; value: number }[]>([]);
+  const [selectedVisible, setSelectedVisible] = useState({ label: 'visited by', value: 0 });
+  const visibleTypes = [
+    { label: 'visited by', value: 0 },
+    { label: 'visited in', value: 1 }
+  ];
+  const [routes] = useState([
+    { key: 'regions', title: 'NM / UN / DARE' },
+    { key: 'series', title: 'Series' }
+  ]);
+  const { data } = usePostGetMapYearsQuery(userId, true);
+  const { data: seriesList } = useGetListQuery(true);
+  const [series, setSeries] = useState<{ label: string; value: number }[]>([]);
+  const [selectedSeries, setSelectedSeries] = useState<number[]>([]);
+  const [seriesVisible, setSeriesVisible] = useState(true);
+  const [selectedSeriesFilter, setSelectedSeriesFilter] = useState(-1);
+
+  useEffect(() => {
+    if (data) {
+      const years = data.data.map_years.filter((year) => year > 1900);
+      const formattedYears = years
+        .map((year) => ({ label: year.toString(), value: year }))
+        .reverse();
+      setAllYears(formattedYears);
+      formattedYears.length && setSelectedYear(formattedYears[0]);
+    }
+  }, [data]);
+
+  useEffect(() => {
+    if (seriesList?.data) {
+      setSeries([
+        ...seriesList.data.map((item) => ({
+          label: item.name,
+          value: item.id,
+          icon: item.icon
+        }))
+      ]);
+      setSelectedSeries(seriesList.data.map((item) => item.id));
+    }
+  }, [seriesList]);
+
+  if (!data) return;
+
+  const handleApplyFilter = () => {
+    let tileUrl = `${FASTEST_MAP_HOST}/tiles_nm/`;
+    if (!selectedYear) {
+      if (tilesType.value === 0) {
+        tileUrl += 'user_visited/' + userId;
+      } else if (tilesType.value === 1) {
+        tileUrl += 'user_visited_un/' + userId;
+      } else {
+        tileUrl += 'user_visited_dare/' + userId;
+      }
+      return;
+    }
+    if (selectedVisible.value === 0) {
+      if (tilesType.value === 0) {
+        tileUrl += 'user_visited_year/' + userId + '/' + selectedYear.value;
+      } else if (tilesType.value === 1) {
+        tileUrl += 'user_visited_un_year/' + userId + '/' + selectedYear.value;
+      } else {
+        tileUrl += 'user_visited_dare/' + userId;
+      }
+    } else {
+      if (tilesType.value === 0) {
+        tileUrl += 'user_visited_in_year/' + userId + '/' + selectedYear.value;
+      } else if (tilesType.value === 1) {
+        tileUrl += 'user_visited_un_in_year/' + userId + '/' + selectedYear.value;
+      } else {
+        tileUrl += 'user_visited_dare/' + userId;
+      }
+    }
+    setVisitedTiles(tileUrl);
+  };
+
+  const renderScene = ({ route }: { route: any }) => {
+    return route.key === 'regions' ? (
+      <View style={styles.sceneContainer}>
+        <View style={styles.optionsContainer}>
+          <View style={styles.rowWrapper}>
+            <View style={[styles.dropdownWrapper, {}]}>
               <Dropdown
                 style={[ModalStyles.dropdown, { width: '100%' }]}
                 placeholderStyle={ModalStyles.placeholderStyle}
@@ -62,43 +137,221 @@ const FilterModal = ({
                 }}
               />
             </View>
-            <View style={[ModalStyles.buttonsWrapper, { marginTop: 24 }]}>
-              <Button
-                variant={ButtonVariants.OPACITY}
-                containerStyles={{
-                  borderColor: Colors.DARK_BLUE,
-                  backgroundColor: 'white',
-                  width: '45%'
-                }}
-                textStyles={{
-                  color: Colors.DARK_BLUE
-                }}
-                onPress={() => {
-                  setTilesType({ label: 'NM regions', value: 0 });
-                }}
-                children={'Clear'}
+          </View>
+
+          {tilesType.value !== 2 && allYears.length ? (
+            <View style={styles.rowWrapper}>
+              <View style={styles.dropdownWrapper}>
+                <Dropdown
+                  style={[ModalStyles.dropdown, { width: '100%' }]}
+                  placeholderStyle={ModalStyles.placeholderStyle}
+                  selectedTextStyle={ModalStyles.selectedTextStyle}
+                  containerStyle={ModalStyles.dropdownContent}
+                  data={visibleTypes}
+                  labelField="label"
+                  valueField="value"
+                  value={selectedVisible?.label}
+                  placeholder={selectedVisible?.label}
+                  onChange={(item) => {
+                    setSelectedVisible(item);
+                  }}
+                />
+              </View>
+              <View style={styles.dropdownWrapper}>
+                <Dropdown
+                  style={[ModalStyles.dropdown, { width: '100%' }]}
+                  placeholderStyle={ModalStyles.placeholderStyle}
+                  selectedTextStyle={ModalStyles.selectedTextStyle}
+                  containerStyle={ModalStyles.dropdownContent}
+                  data={allYears}
+                  labelField="label"
+                  valueField="value"
+                  value={selectedYear?.label}
+                  placeholder={selectedYear?.label}
+                  dropdownPosition="top"
+                  onChange={(item) => {
+                    setSelectedYear(item);
+                  }}
+                />
+              </View>
+            </View>
+          ) : null}
+        </View>
+        <View style={{ gap: 16 }}>
+          <Button
+            children="Filter"
+            onPress={() => {
+              handleApplyFilter();
+              setType(tilesType.value);
+              setIsFilterVisible(false);
+            }}
+          />
+          <Button
+            children="Clear"
+            onPress={() => {
+              setTilesType({ label: 'NM regions', value: 0 });
+              setSelectedYear(allYears[0]);
+              setSelectedVisible({ label: 'visited by', value: 0 });
+              setVisitedTiles(`${FASTEST_MAP_HOST}/tiles_nm/user_visited/${userId}`);
+              setType(0);
+            }}
+            variant={ButtonVariants.OPACITY}
+            containerStyles={styles.closeBtn}
+            textStyles={{ color: Colors.DARK_BLUE }}
+          />
+        </View>
+      </View>
+    ) : (
+      <View style={styles.sceneContainer}>
+        <View style={styles.optionsContainer}>
+          <View style={[styles.row, { gap: 8 }]}>
+            <Switch
+              trackColor={{ false: Colors.LIGHT_GRAY, true: Colors.DARK_BLUE }}
+              thumbColor={Colors.WHITE}
+              onValueChange={setSeriesVisible}
+              value={seriesVisible}
+              style={{ transform: 'scale(0.8)' }}
+            />
+            <Text style={styles.textBold}>Visible / Not visible</Text>
+          </View>
+
+          <View style={[styles.row, { justifyContent: 'space-between' }]}>
+            <TouchableOpacity style={styles.row} onPress={() => setSelectedSeriesFilter(-1)}>
+              <RadioButton.Android
+                value="all"
+                status={selectedSeriesFilter === -1 ? 'checked' : 'unchecked'}
+                onPress={() => setSelectedSeriesFilter(-1)}
+                color={Colors.DARK_BLUE}
               />
-              <Button
-                variant={ButtonVariants.FILL}
-                containerStyles={{
-                  borderColor: Colors.DARK_BLUE,
-                  backgroundColor: Colors.DARK_BLUE,
-                  width: '45%'
-                }}
-                textStyles={{
-                  color: Colors.WHITE
-                }}
-                onPress={() => {
-                  setType(tilesType.value);
-                  setIsFilterVisible(false);
-                }}
-                children={'Filter'}
+              <Text style={styles.textBold}>All items</Text>
+            </TouchableOpacity>
+
+            <TouchableOpacity style={styles.row} onPress={() => setSelectedSeriesFilter(1)}>
+              <RadioButton.Android
+                value="visited"
+                status={selectedSeriesFilter === 1 ? 'checked' : 'unchecked'}
+                onPress={() => setSelectedSeriesFilter(1)}
+                color={Colors.DARK_BLUE}
               />
-            </View>
+              <Text style={styles.textBold}>Visited</Text>
+            </TouchableOpacity>
+
+            <TouchableOpacity style={styles.row} onPress={() => setSelectedSeriesFilter(0)}>
+              <RadioButton.Android
+                value="not-visited"
+                status={selectedSeriesFilter === 0 ? 'checked' : 'unchecked'}
+                onPress={() => setSelectedSeriesFilter(0)}
+                color={Colors.DARK_BLUE}
+              />
+              <Text style={styles.textBold}>Not visited</Text>
+            </TouchableOpacity>
           </View>
+
+          <MultiSelect
+            style={[ModalStyles.dropdown, { width: '100%' }]}
+            placeholderStyle={[ModalStyles.placeholderStyle]}
+            selectedTextStyle={ModalStyles.selectedTextStyle}
+            containerStyle={[ModalStyles.dropdownContent, {}]}
+            data={series}
+            labelField="label"
+            valueField="value"
+            value={selectedSeries}
+            placeholder="Select series"
+            dropdownPosition="top"
+            activeColor="#E7E7E7"
+            flatListProps={{ initialNumToRender: 30, maxToRenderPerBatch: 10 }}
+            onChange={(item) => {
+              setSelectedSeries(item);
+            }}
+            renderItem={(item) => (
+              <View style={styles.multiOption}>
+                <View style={[styles.row, { gap: 8, flex: 1 }]}>
+                  <Image source={{ uri: API_HOST + item.icon }} width={20} height={20} />
+                  <Text style={styles.optionText}>{item.label}</Text>
+                </View>
+
+                {selectedSeries.includes(item.value) && (
+                  <CheckSvg fill={Colors.DARK_BLUE} height={8} />
+                )}
+              </View>
+            )}
+            renderSelectedItem={(item, unSelect) => {
+              return null;
+            }}
+          />
+        </View>
+        <View style={{ gap: 16 }}>
+          <Button
+            children="Filter"
+            onPress={() => {
+              setSeriesFilter &&
+                setSeriesFilter({
+                  visible: seriesVisible,
+                  groups: selectedSeries,
+                  applied: true,
+                  status: selectedSeriesFilter
+                });
+              setIsFilterVisible(false);
+            }}
+          />
+          <Button
+            children="Clear"
+            onPress={() => {
+              setSelectedSeries(series?.map((item) => item.value));
+              setSelectedSeriesFilter(-1);
+              setSeriesVisible(true);
+              setSeriesFilter &&
+                setSeriesFilter({ visible: true, groups: [], applied: false, status: -1 });
+            }}
+            variant={ButtonVariants.OPACITY}
+            containerStyles={styles.closeBtn}
+            textStyles={{ color: Colors.DARK_BLUE }}
+          />
         </View>
       </View>
-    </Modal>
+    );
+  };
+
+  return (
+    <ReactModal
+      isVisible={isFilterVisible}
+      onBackdropPress={() => setIsFilterVisible(false)}
+      onBackButtonPress={() => setIsFilterVisible(false)}
+      style={styles.modal}
+      statusBarTranslucent={true}
+      presentationStyle="overFullScreen"
+    >
+      <View style={[styles.modalContainer, { height: 400 }]}>
+        <TabView
+          navigationState={{ index, routes }}
+          renderScene={renderScene}
+          onIndexChange={setIndex}
+          lazy={true}
+          swipeEnabled={!isPublicView}
+          renderTabBar={(props) =>
+            !isPublicView ? (
+              <TabBar
+                {...props}
+                indicatorStyle={{ backgroundColor: Colors.DARK_BLUE }}
+                style={styles.tabBar}
+                tabStyle={styles.tabStyle}
+                pressColor={'transparent'}
+                renderLabel={({ route, focused }) => (
+                  <Text
+                    style={[
+                      styles.tabLabel,
+                      { color: Colors.DARK_BLUE, opacity: focused ? 1 : 0.4 }
+                    ]}
+                  >
+                    {route.title}
+                  </Text>
+                )}
+              />
+            ) : null
+          }
+        />
+      </View>
+    </ReactModal>
   );
 };
 

+ 70 - 0
src/screens/InAppScreens/MapScreen/FilterModal/styles.tsx

@@ -0,0 +1,70 @@
+import { StyleSheet, Dimensions } from 'react-native';
+import { Colors } from '../../../../theme';
+import { getFontSize } from 'src/utils';
+
+export const styles = StyleSheet.create({
+  tabBar: {
+    backgroundColor: 'transparent',
+    elevation: 0,
+    shadowOpacity: 0,
+    marginTop: 0,
+    height: 42
+  },
+  tabLabel: {
+    color: 'grey',
+    fontSize: getFontSize(14),
+    fontWeight: '700',
+    padding: 8,
+    width: Dimensions.get('window').width / 3,
+    textAlign: 'center'
+  },
+  tabStyle: {
+    padding: 0,
+    marginHorizontal: 2
+  },
+  modal: {
+    justifyContent: 'flex-end',
+    margin: 0
+  },
+  modalContainer: {
+    backgroundColor: 'white',
+    borderRadius: 15,
+    paddingHorizontal: 16,
+    gap: 12,
+    paddingTop: 16
+  },
+  dropdownWrapper: { gap: 4, flex: 1 },
+  textSmall: {
+    color: Colors.DARK_BLUE,
+    fontWeight: '600',
+    fontSize: 12
+  },
+  rowWrapper: {
+    width: '100%',
+    display: 'flex',
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    gap: 16
+  },
+  optionsContainer: {
+    gap: 16,
+    marginBottom: 16
+  },
+  closeBtn: { backgroundColor: Colors.WHITE, borderColor: '#B7C6CB' },
+  sceneContainer: {
+    flex: 1,
+    paddingVertical: 24,
+    justifyContent: 'space-between'
+  },
+  row: { flexDirection: 'row', alignItems: 'center' },
+  textBold: { fontSize: 12, fontWeight: '700', color: Colors.DARK_BLUE },
+  multiOption: {
+    paddingHorizontal: 12,
+    paddingVertical: 8,
+    justifyContent: 'space-between',
+    flexDirection: 'row',
+    alignItems: 'center'
+  },
+  optionText: { fontSize: 16, color: Colors.DARK_BLUE, flex: 1 }
+});

+ 31 - 8
src/screens/InAppScreens/MapScreen/index.tsx

@@ -90,8 +90,7 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
   const { mutateAsync: mutateUserDataDare } = fetchUserDataDare();
   const { mutateAsync: mutateCountriesData } = fetchCountryUserData();
   const { mutate: updateSeriesItem } = usePostSetToggleItem();
-  const visitedTiles = `${FASTEST_MAP_HOST}/tiles_nm/user_visited/${userId}`;
-  const visitedUNTiles = `${FASTEST_MAP_HOST}/tiles_nm/user_visited_un/${userId}`;
+  const visitedDefaultTiles = `${FASTEST_MAP_HOST}/tiles_nm/user_visited/${userId}`;
   const mapRef = useRef<MapView>(null);
 
   const [isConnected, setIsConnected] = useState<boolean | null>(true);
@@ -125,9 +124,17 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
   const [tilesType, setTilesType] = useState({ label: 'NM regions', value: 0 });
   const tilesTypes = [
     { label: 'NM regions', value: 0 },
-    { label: 'UN countries', value: 1 }
+    { label: 'UN countries', value: 1 },
+    { label: 'DARE places', value: 2 }
   ];
   const [type, setType] = useState(0);
+  const [visitedTiles, setVisitedTiles] = useState(visitedDefaultTiles);
+  const [seriesFilter, setSeriesFilter] = useState<any>({
+    visible: true,
+    groups: [],
+    applied: false,
+    status: -1
+  });
 
   const { handleUpdateNM, handleUpdateDare, handleUpdateSlow, userData, setUserData } = useRegion();
   const userInfo = storage.get('currentUserData', StoreType.STRING) as string;
@@ -578,7 +585,7 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
 
   const renderMapTiles = (url: string, cacheDir: string, zIndex: number, opacity = 1) => (
     <UrlTile
-      key={`${url}-${cacheDir}`}
+      key={`${url}-${cacheDir}-${zoomLevel > 6 ? 0 : 1}`}
       urlTemplate={url === tilesBaseURL && zoomLevel > 13 ? openstreetmapUrl : `${url}/{z}/{x}/{y}`}
       maximumZ={18}
       maximumNativeZ={url === tilesBaseURL ? 18 : 13}
@@ -661,7 +668,20 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
   );
 
   const renderMarkers = () => {
-    return markers.map((marker, idx) => {
+    let filteredMarkers = markers;
+
+    if (seriesFilter.applied) {
+      filteredMarkers = filteredMarkers.filter((marker) =>
+        seriesFilter.groups.includes(marker.series_id)
+      );
+      if (seriesFilter.status !== -1) {
+        filteredMarkers = filteredMarkers.filter(
+          (marker) => marker.visited === seriesFilter.status
+        );
+      }
+    }
+
+    return filteredMarkers.map((marker, idx) => {
       const coordinate = { latitude: marker.pointJSON[0], longitude: marker.pointJSON[1] };
       const markerSeries = series?.find((s) => s.id === marker.series_id);
       const iconUrl = markerSeries ? API_HOST + markerSeries.icon : 'default_icon_url';
@@ -740,15 +760,14 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
         {renderedGeoJSON}
         {renderMapTiles(tilesBaseURL, localTileDir, 1)}
         {type !== 1 && renderMapTiles(gridUrl, localGridDir, 2)}
-        {userId &&
-          renderMapTiles(type === 1 ? visitedUNTiles : visitedTiles, localVisitedDir, 2, 0.5)}
+        {userId && renderMapTiles(visitedTiles, localVisitedDir, 3, 0.5)}
         {renderMapTiles(dareTiles, localDareDir, 2, 0.5)}
         {location && (
           <AnimatedMarker coordinate={location} anchor={{ x: 0.5, y: 0.5 }}>
             <Animation.View style={[styles.location, { borderWidth: strokeWidthAnim }]} />
           </AnimatedMarker>
         )}
-        {markers && renderMarkers()}
+        {markers && seriesFilter.visible && renderMarkers()}
       </ClusteredMapView>
 
       <LocationPopup
@@ -913,6 +932,10 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
         setTilesType={setTilesType}
         type={type}
         setType={setType}
+        userId={userId ? +userId : 0}
+        setVisitedTiles={setVisitedTiles}
+        setSeriesFilter={setSeriesFilter}
+        isPublicView={false}
       />
       <EditModal
         isVisible={isEditSlowModalVisible}

+ 13 - 6
src/screens/InAppScreens/ProfileScreen/UsersMap/index.tsx

@@ -23,17 +23,18 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
 
   const tilesBaseURL = `${FASTEST_MAP_HOST}/tiles_osm`;
   const gridUrl = `${FASTEST_MAP_HOST}/tiles_nm/grid`;
-  const visitedTiles = `${FASTEST_MAP_HOST}/tiles_nm/user_visited/${userId}`;
-  const visitedUNTiles = `${FASTEST_MAP_HOST}/tiles_nm/user_visited_un/${userId}`;
+  const visitedDefaultTiles = `${FASTEST_MAP_HOST}/tiles_nm/user_visited/${userId}`;
 
   const mapRef = useRef<MapView>(null);
   const [isFilterVisible, setIsFilterVisible] = useState(false);
   const [tilesType, setTilesType] = useState({ label: 'NM regions', value: 0 });
   const tilesTypes = [
     { label: 'NM regions', value: 0 },
-    { label: 'UN countries', value: 1 }
+    { label: 'UN countries', value: 1 },
+    { label: 'DARE places', value: 2 }
   ];
   const [type, setType] = useState(0);
+  const [visitedTiles, setVisitedTiles] = useState(visitedDefaultTiles);
 
   useEffect(() => {
     navigation.getParent()?.setOptions({
@@ -85,8 +86,8 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
         minZoomLevel={0}
       >
         {renderMapTiles(tilesBaseURL, 1)}
-        {renderMapTiles(gridUrl, 2)}
-        {userId && renderMapTiles(type === 1 ? visitedUNTiles : visitedTiles, 2, 0.5)}
+        {type !== 1 && renderMapTiles(gridUrl, 2)}
+        {userId && renderMapTiles(visitedTiles, 2, 0.5)}
       </MapView>
 
       <TouchableOpacity
@@ -98,7 +99,10 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
       </TouchableOpacity>
       <View style={[styles.cornerButton, styles.topRightButton]}>
         {data.user_data.avatar ? (
-          <Image style={styles.avatar} source={{ uri: API_HOST + '/img/avatars/' + 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] ?? ''}`}
@@ -122,6 +126,9 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
         setTilesType={setTilesType}
         type={type}
         setType={setType}
+        userId={userId}
+        setVisitedTiles={setVisitedTiles}
+        isPublicView={true}
       />
     </View>
   );

+ 4 - 1
src/screens/InAppScreens/TravellersScreen/MasterRankingScreen/index.tsx

@@ -34,7 +34,10 @@ const MasterRankingScreen = () => {
       const fetchRanking = async () => {
         const ranking = storage.get('masterRanking', StoreType.STRING) as string;
         setMasterRanking(
-          JSON.parse(ranking).sort((a: Ranking, b: Ranking) => b.score_nm - a.score_nm)
+          JSON.parse(ranking).sort(
+            (a: Ranking, b: Ranking) =>
+              b.score_nm - a.score_nm || b.score_un - a.score_un || a.age - b.age
+          )
         );
         setIsLoading(false);
       };

+ 55 - 11
src/screens/InAppScreens/TravellersScreen/utils/sort.ts

@@ -25,38 +25,60 @@ export const applyModalSort = (
     switch (ranking?.label) {
       case 'NM':
         filteredLocalData = filteredLocalData.sort(
-          (a: Ranking, b: Ranking) => b.score_nm - a.score_nm
+          (a: Ranking, b: Ranking) =>
+            b.score_nm - a.score_nm || b.score_un - a.score_un || a.age - b.age
         );
         break;
       case 'DARE':
         filteredLocalData = filteredLocalData.sort(
-          (a: Ranking, b: Ranking) => b.score_dare - a.score_dare
+          (a: Ranking, b: Ranking) =>
+            b.score_dare - a.score_dare ||
+            b.score_nm - a.score_nm ||
+            b.score_un - a.score_un ||
+            a.age - b.age
         );
         break;
       case 'UN':
         filteredLocalData = filteredLocalData.sort(
-          (a: Ranking, b: Ranking) => b.score_un - a.score_un
+          (a: Ranking, b: Ranking) =>
+            b.score_un - a.score_un || b.score_nm - a.score_nm || a.age - b.age
         );
         break;
       case 'UN+':
         filteredLocalData = filteredLocalData.sort(
-          (a: Ranking, b: Ranking) => b.score_unp - a.score_unp
+          (a: Ranking, b: Ranking) =>
+            b.score_unp - a.score_unp ||
+            b.score_nm - a.score_nm ||
+            b.score_un - a.score_un ||
+            a.age - b.age
         );
         break;
       case 'TCC':
         filteredLocalData = filteredLocalData.sort(
-          (a: Ranking, b: Ranking) => b.score_tcc - a.score_tcc
+          (a: Ranking, b: Ranking) =>
+            b.score_tcc - a.score_tcc ||
+            b.score_nm - a.score_nm ||
+            b.score_un - a.score_un ||
+            a.age - b.age
         );
         break;
       case 'DEEP':
         filteredLocalData = filteredLocalData.sort(
-          (a: Ranking, b: Ranking) => b?.score_deep - a?.score_deep
+          (a: Ranking, b: Ranking) =>
+            b?.score_deep - a?.score_deep ||
+            b.score_nm - a.score_nm ||
+            b.score_un - a.score_un ||
+            a.age - b.age
         );
         break;
       case 'YES':
         const YESFilteredUsers = filteredLocalData.filter((user) => user.score_yes !== 10000);
         filteredLocalData = YESFilteredUsers.sort(
-          (a: Ranking, b: Ranking) => a.score_yes - b.score_yes
+          (a: Ranking, b: Ranking) =>
+            a.score_yes - b.score_yes ||
+            b.score_nm - a.score_nm ||
+            b.score_un - a.score_un ||
+            a.age - b.age
         );
         break;
       case 'SLOW':
@@ -64,22 +86,44 @@ export const applyModalSort = (
           (user) => user.score_slow < 4500 && user.score_slow > 0
         );
         filteredLocalData = SLOWFilteredUsers.sort(
-          (a: Ranking, b: Ranking) => b.score_slow - a.score_slow
+          (a: Ranking, b: Ranking) =>
+            b.score_slow - a.score_slow ||
+            b.score_nm - a.score_nm ||
+            b.score_un - a.score_un ||
+            a.age - b.age
         );
         break;
       case 'WHS':
         filteredLocalData = filteredLocalData.sort(
-          (a: Ranking, b: Ranking) => b.score_whs - a.score_whs
+          (a: Ranking, b: Ranking) =>
+            b.score_whs - a.score_whs ||
+            b.score_nm - a.score_nm ||
+            b.score_un - a.score_un ||
+            a.age - b.age
         );
         break;
       case 'KYE':
         filteredLocalData = filteredLocalData.sort(
-          (a: Ranking, b: Ranking) => b.score_kye - a.score_kye
+          (a: Ranking, b: Ranking) =>
+            b.score_kye - a.score_kye ||
+            b.score_nm - a.score_nm ||
+            b.score_un - a.score_un ||
+            a.age - b.age
         );
         break;
       case 'TBT':
         filteredLocalData = filteredLocalData.sort(
-          (a: Ranking, b: Ranking) => b.score_tbt - a.score_tbt
+          (a: Ranking, b: Ranking) =>
+            b.score_tbt - a.score_tbt ||
+            b.score_nm - a.score_nm ||
+            b.score_un - a.score_un ||
+            a.age - b.age
+        );
+        break;
+      default:
+        filteredLocalData = filteredLocalData.sort(
+          (a: Ranking, b: Ranking) =>
+            b.score_nm - a.score_nm || b.score_un - a.score_un || a.age - b.age
         );
         break;
     }

+ 6 - 2
src/types/api.ts

@@ -108,7 +108,9 @@ export enum API_ENDPOINT {
   GET_COUNTRY_SCREEN_DATA = 'get-country-screen-data',
   GET_USERS_FROM_COUNTRY = 'get-users-from-country',
   GET_USERS_WHO_VISITED_COUNTRY = 'get-users-who-visited-country',
-  GET_COUNTRY_USER_DATA = 'get-user-data-country-app'
+  GET_COUNTRY_USER_DATA = 'get-user-data-country-app',
+  GET_MAP_YEARS = 'get-map-years',
+  GET_SERIES_LIST = 'get-list'
 }
 
 export enum API {
@@ -196,7 +198,9 @@ export enum API {
   GET_COUNTRY_SCREEN_DATA = `${API_ROUTE.COUNTRIES}/${API_ENDPOINT.GET_COUNTRY_SCREEN_DATA}`,
   GET_USERS_FROM_COUNTRY = `${API_ROUTE.COUNTRIES}/${API_ENDPOINT.GET_USERS_FROM_COUNTRY}`,
   GET_USERS_WHO_VISITED_COUNTRY = `${API_ROUTE.COUNTRIES}/${API_ENDPOINT.GET_USERS_WHO_VISITED_COUNTRY}`,
-  GET_COUNTRY_USER_DATA = `${API_ROUTE.COUNTRIES}/${API_ENDPOINT.GET_COUNTRY_USER_DATA}`
+  GET_COUNTRY_USER_DATA = `${API_ROUTE.COUNTRIES}/${API_ENDPOINT.GET_COUNTRY_USER_DATA}`,
+  GET_MAP_YEARS = `${API_ROUTE.PROFILE}/${API_ENDPOINT.GET_MAP_YEARS}`,
+  GET_SERIES_LIST = `${API_ROUTE.SERIES}/${API_ENDPOINT.GET_SERIES_LIST}`
 }
 
 export type BaseAxiosError = AxiosError;