Viktoriia hai 7 meses
pai
achega
da6fbfea89

+ 3 - 1
src/modules/api/maps/maps-api.ts

@@ -12,5 +12,7 @@ export const mapsApi = {
   getVisitedCountriesIds: (token: string, type: 'in' | 'by', year: number, uid: number) =>
     request.postForm<PostGetVisitedIds>(API.GET_VISITED_COUNTRIES_IDS, { token, type, year, uid }),
   getVisitedDareIds: (token: string, uid: number) =>
-    request.postForm<PostGetVisitedIds>(API.GET_VISITED_DARE_IDS, { token, uid })
+    request.postForm<PostGetVisitedIds>(API.GET_VISITED_DARE_IDS, { token, uid }),
+  getVisitedSeriesIds: (token: string) =>
+    request.postForm<PostGetVisitedIds>(API.GET_VISITED_SERIES_IDS, { token })
 };

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

@@ -13,5 +13,6 @@ export const mapsQueryKeys = {
     year,
     uid
   ],
-  getVisitedDareIds: (token: string, uid: number) => ['getVisitedDareIds', token, uid]
+  getVisitedDareIds: (token: string, uid: number) => ['getVisitedDareIds', token, uid],
+  getVisitedSeriesIds: (token: string) => ['getVisitedSeriesIds', token]
 };

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

@@ -1,3 +1,4 @@
 export * from './use-post-get-visited-regions-ids';
 export * from './use-post-get-visited-countries-ids';
 export * from './use-post-get-visited-dare-ids';
+export * from './use-post-get-visited-series-ids';

+ 17 - 0
src/modules/api/maps/queries/use-post-get-visited-series-ids.tsx

@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { mapsQueryKeys } from '../maps-query-keys';
+import { mapsApi, type PostGetVisitedIds } from '../maps-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const usePostGetVisitedSeriesIdsQuery = (token: string, enabled: boolean) => {
+  return useQuery<PostGetVisitedIds, BaseAxiosError>({
+    queryKey: mapsQueryKeys.getVisitedSeriesIds(token),
+    queryFn: async () => {
+      const response = await mapsApi.getVisitedSeriesIds(token);
+      return response.data;
+    },
+    enabled
+  });
+};

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

@@ -9,3 +9,4 @@ 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';
+export * from './use-get-icons';

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

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

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

@@ -150,6 +150,14 @@ export type SubmitSuggestionTypes = {
   item: number;
 };
 
+export interface PostGetSeriesIcons extends ResponseType {
+  data: {
+    id: number;
+    new_icon_png: string;
+    new_icon_visited_png: string;
+  }[];
+}
+
 export const seriesApi = {
   getSeries: (token: string | null, regions: string) =>
     request.postForm<PostGetSeries>(API.SERIES, { token, regions }),
@@ -185,5 +193,6 @@ export const seriesApi = {
   getSuggestionData: () => request.postForm<PostGetSuggestionData>(API.GET_SUGGESTION_DATA),
   submitSuggestion: (data: SubmitSuggestionTypes) =>
     request.postForm<SubmitSuggestionReturn>(API.SUBMIT_SUGGESTION, data),
-  getList: () => request.postForm<PostGetSeriesList>(API.GET_SERIES_LIST)
+  getList: () => request.postForm<PostGetSeriesList>(API.GET_SERIES_LIST),
+  getIcons: () => request.postForm<PostGetSeriesIcons>(API.GET_ICONS)
 };

+ 7 - 3
src/modules/api/series/series-query-keys.tsx

@@ -2,12 +2,16 @@ export const seriesQueryKeys = {
   fetchSeriesData: () => ['fetchSeriesData'] as const,
   getSeriesGroups: () => ['getSeriesGroups'] as const,
   getSeriesWithGroup: () => ['getSeriesWithGroup'] as const,
-  getItemsForSeries: (token: string, series_id: string) => ['getItemsForSeries', {token, series_id}] as const,
+  getItemsForSeries: (token: string, series_id: string) =>
+    ['getItemsForSeries', { token, series_id }] as const,
   setToggleItem: () => ['setToggleItem'] as const,
   getSeriesGroupsRanking: () => ['getSeriesGroupsRanking'] as const,
-  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,
+  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,
   getList: () => ['getList'] as const,
+  getIcons: () => ['getIcons'] as const
 };

+ 2 - 1
src/screens/InAppScreens/MapScreen/FilterModal/index.tsx

@@ -14,6 +14,7 @@ import CheckSvg from 'assets/icons/mark.svg';
 import { useGetListQuery } from '@api/series';
 import { RadioButton } from 'react-native-paper';
 import { storage, StoreType } from 'src/storage';
+import moment from 'moment';
 
 const FilterModal = ({
   isFilterVisible,
@@ -270,7 +271,7 @@ const FilterModal = ({
               setTilesType({ label: 'NM regions', value: 0 });
               setSelectedYear(allYears[0]);
               setSelectedVisible({ label: 'visited by', value: 0 });
-              setRegionsFilter({ visitedLabel: 'by', year: allYears[0].value });
+              setRegionsFilter({ visitedLabel: 'by', year: moment().year() });
               setType('regions');
               if (!isPublicView && isLogged) {
                 storage.set(

+ 72 - 39
src/screens/InAppScreens/MapScreen/MarkerItem/index.tsx

@@ -1,58 +1,98 @@
 import { useEffect, useRef } from 'react';
 import { View, Image, Text, TouchableOpacity, Platform } from 'react-native';
-import { Marker, Callout, CalloutSubview, MapMarker } from 'react-native-maps';
-import CustomCallout from '../CustomCallout';
 
 import { styles } from './styles';
-import { ItemSeries } from '../../../../types/map';
 import { Colors } from 'src/theme';
 
 import CheckSvg from 'assets/icons/mark.svg';
+import MapLibreGL, { PointAnnotationRef } from '@maplibre/maplibre-react-native';
 
 const MarkerItem = ({
   marker,
-  iconUrl,
-  coordinate,
-  seriesName,
   toggleSeries,
   token
 }: {
-  marker: ItemSeries;
-  iconUrl: string;
-  coordinate: { latitude: number; longitude: number };
-  seriesName: string;
+  marker: any;
   toggleSeries: (item: any) => void;
   token: string;
 }) => {
-  let markerRef = useRef<MapMarker>(null);
+  const calloutRef = useRef<PointAnnotationRef>(null);
   useEffect(() => {
-    if (markerRef.current && Platform.OS !== 'ios') {
-      markerRef.current?.showCallout();
+    if (Platform.OS === 'android') {
+      calloutRef.current?.refresh();
     }
-  }, [marker.visited]);
-
+  }, [marker]);
   return (
     <>
-      <Marker coordinate={coordinate} tracksViewChanges={false} ref={markerRef}>
-        <View
-          style={[
-            styles.markerContainer,
-            (marker.visited === 1 && token && { backgroundColor: Colors.ORANGE }) || {}
-          ]}
+      {Platform.OS === 'ios' ? (
+        <MapLibreGL.PointAnnotation
+          id="selected_marker_callout"
+          coordinate={marker.coordinates}
+          anchor={{ x: 0.5, y: 1 }}
+        >
+          <View style={styles.customView}>
+            <View style={styles.calloutContainer}>
+              <View style={styles.calloutImageContainer}>
+                <Image
+                  source={{ uri: marker.icon.uri }}
+                  style={styles.calloutImage}
+                  resizeMode="contain"
+                />
+              </View>
+              <View style={styles.calloutTextContainer}>
+                <Text style={styles.seriesName}>{marker.series_name}</Text>
+                <Text style={styles.markerName}>{marker.name}</Text>
+              </View>
+              <TouchableOpacity
+                style={[
+                  styles.calloutButton,
+                  (marker.visited === 1 &&
+                    token && {
+                      backgroundColor: Colors.WHITE,
+                      borderWidth: 1,
+                      borderColor: Colors.BORDER_LIGHT
+                    }) ||
+                    {}
+                ]}
+                onPress={() => toggleSeries(marker)}
+              >
+                {marker?.visited === 1 && token ? (
+                  <View style={styles.completedContainer}>
+                    <CheckSvg width={14} height={14} fill={Colors.DARK_BLUE} />
+                    <Text style={[styles.calloutButtonText, { color: Colors.DARK_BLUE }]}>
+                      Completed
+                    </Text>
+                  </View>
+                ) : (
+                  <Text style={styles.calloutButtonText}>Mark Completed</Text>
+                )}
+              </TouchableOpacity>
+            </View>
+          </View>
+        </MapLibreGL.PointAnnotation>
+      ) : (
+        <MapLibreGL.PointAnnotation
+          id="selected_marker_callout"
+          coordinate={marker.coordinates}
+          anchor={{ x: 0.5, y: 0.9 }}
+          onSelected={() => toggleSeries(marker)}
+          selected={true}
+          ref={calloutRef}
         >
-          <Image source={{ uri: iconUrl }} style={styles.icon} resizeMode="contain" />
-        </View>
-        {Platform.OS === 'ios' ? (
-          <Callout tooltip style={styles.customView}>
+          <View style={styles.customView}>
             <View style={styles.calloutContainer}>
               <View style={styles.calloutImageContainer}>
-                <Image source={{ uri: iconUrl }} style={styles.calloutImage} resizeMode="contain" />
+                <Image
+                  source={{ uri: marker.icon.uri }}
+                  style={styles.calloutImage}
+                  resizeMode="contain"
+                />
               </View>
               <View style={styles.calloutTextContainer}>
-                <Text style={styles.seriesName}>{seriesName}</Text>
+                <Text style={styles.seriesName}>{marker.series_name}</Text>
                 <Text style={styles.markerName}>{marker.name}</Text>
               </View>
-              <CalloutSubview
+              <TouchableOpacity
                 style={[
                   styles.calloutButton,
                   (marker.visited === 1 &&
@@ -75,18 +115,11 @@ const MarkerItem = ({
                 ) : (
                   <Text style={styles.calloutButtonText}>Mark Completed</Text>
                 )}
-              </CalloutSubview>
+              </TouchableOpacity>
             </View>
-          </Callout>
-        ) : (
-          <CustomCallout
-            marker={marker}
-            toggleSeries={toggleSeries}
-            seriesName={seriesName}
-            token={token}
-          />
-        )}
-      </Marker>
+          </View>
+        </MapLibreGL.PointAnnotation>
+      )}
     </>
   );
 };

+ 3 - 3
src/screens/InAppScreens/MapScreen/MarkerItem/styles.tsx

@@ -40,11 +40,11 @@ export const styles = StyleSheet.create({
     borderRadius: 19,
     borderWidth: 2,
     borderColor: Colors.TEXT_GRAY,
-    marginTop: -34
+    marginTop: Platform.OS === 'ios' ? -34 : -4
   },
   calloutImage: {
-    width: 28,
-    height: 28
+    width: 32,
+    height: 32
   },
   calloutTextContainer: {
     flex: 1,

+ 198 - 176
src/screens/InAppScreens/MapScreen/index.tsx

@@ -9,7 +9,7 @@ import {
   Image,
   StatusBar
 } from 'react-native';
-import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react';
+import React, { useEffect, useRef, useState, useCallback } from 'react';
 
 import MapLibreGL, { CameraRef, MapViewRef } from '@maplibre/maplibre-react-native';
 import { styles } from './style';
@@ -28,8 +28,8 @@ import ProfileIcon from 'assets/icons/bottom-navigation/profile.svg';
 import RegionPopup from 'src/components/RegionPopup';
 import { useRegion } from 'src/contexts/RegionContext';
 import { qualityOptions } from '../TravelsScreen/utils/constants';
-import { AvatarWithInitials, EditNmModal, LocationPopup, WarningModal } from 'src/components';
-import { API_HOST } from 'src/constants';
+import { AvatarWithInitials, EditNmModal, WarningModal } from 'src/components';
+import { API_HOST, MAP_HOST } from 'src/constants';
 import { NAVIGATION_PAGES } from 'src/types';
 import Animated, {
   Easing,
@@ -57,13 +57,16 @@ import moment from 'moment';
 import {
   usePostGetVisitedCountriesIdsQuery,
   usePostGetVisitedDareIdsQuery,
-  usePostGetVisitedRegionsIdsQuery
+  usePostGetVisitedRegionsIdsQuery,
+  usePostGetVisitedSeriesIdsQuery
 } from '@api/maps';
 import FilterModal from './FilterModal';
 import { useGetListDareQuery } from '@api/myDARE';
+import { useGetIconsQuery, usePostSetToggleItem } from '@api/series';
+import MarkerItem from './MarkerItem';
 
 MapLibreGL.setAccessToken(null);
-// MapLibreGL.Logger.setLogLevel('error');
+MapLibreGL.Logger.setLogLevel('error');
 
 const generateFilter = (ids: number[]) => {
   return ids?.length ? ['any', ...ids.map((id) => ['==', 'id', id])] : ['==', 'id', -1];
@@ -95,7 +98,7 @@ let countries_visited = {
     fillOutlineColor: 'rgba(14, 80, 109, 1)'
   },
   filter: generateFilter([]),
-  maxzoom: 12
+  maxzoom: 10
 };
 
 let dare_visited = {
@@ -163,46 +166,43 @@ let selected_region = {
   maxzoom: 12
 };
 
-let series_layer_cluster = {
-  id: 'series_layer_cluster',
+let series_layer = {
+  id: 'series_layer',
   type: 'symbol',
-  source: 'nomadmania_series_cluster',
+  source: 'nomadmania_series',
   'source-layer': 'series',
-  minzoom: 0,
-  maxzoom: 10,
-  filter: ['==', 'clustered', true],
   layout: {
-    'icon-image': 'series',
+    'icon-image': '{series_id}',
     'icon-size': 0.1,
     'text-anchor': 'top',
-    'text-field': '{point_count}',
-    'text-font': ['Noto Sans Bold'],
+    'text-field': '{series_name} - {name}',
+    'text-font': ['Noto Sans Regular'],
     'text-max-width': 9,
     'text-offset': [0, 0.6],
     'text-padding': 2,
     'text-size': 12,
     visibility: 'visible',
     'text-optional': true,
-    'text-ignore-placement': true
+    'text-ignore-placement': true,
+    'text-allow-overlap': true
   },
   paint: {
     'text-color': '#666',
     'text-halo-blur': 0.5,
     'text-halo-color': '#ffffff',
     'text-halo-width': 1
-  }
+  },
+  filter: generateFilter([])
 };
 
-let series_layer = {
-  id: 'series_layer',
+let series_visited = {
+  id: 'series_visited',
   type: 'symbol',
   source: 'nomadmania_series',
   'source-layer': 'series',
-  minzoom: 0,
-  maxzoom: 24,
   layout: {
-    'icon-image': '{series_id}',
-    'icon-size': 0.1,
+    'icon-image': '{series_id}v',
+    'icon-size': 0.15,
     'text-anchor': 'top',
     'text-field': '{series_name} - {name}',
     'text-font': ['Noto Sans Regular'],
@@ -221,7 +221,7 @@ let series_layer = {
     'text-halo-color': '#ffffff',
     'text-halo-width': 1
   },
-  filter: ['!=', 'clustered', true]
+  filter: generateFilter([])
 };
 
 const INITIAL_REGION = {
@@ -277,6 +277,8 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
     +userId,
     type === 'dare' && !!userId
   );
+  const { data: visitedSeriesIds } = usePostGetVisitedSeriesIdsQuery(token, !!userId);
+  const { data: seriesIcons } = useGetIconsQuery(true);
   const userInfo = storage.get('currentUserData', StoreType.STRING) as string;
   const { mutateAsync: mutateUserData } = fetchUserData();
   const { mutateAsync: mutateUserDataDare } = fetchUserDataDare();
@@ -289,7 +291,7 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
   const [location, setLocation] = useState<any | null>(null);
   const [userAvatars, setUserAvatars] = useState<string[]>([]);
   const [userInfoData, setUserInfoData] = useState<any>(null);
-  const [selectedMarker, setSelectedMarker] = useState(null);
+  const [selectedMarker, setSelectedMarker] = useState<any>(null);
 
   const [askLocationVisible, setAskLocationVisible] = useState<boolean>(false);
   const [openSettingsVisible, setOpenSettingsVisible] = useState<boolean>(false);
@@ -326,9 +328,38 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
   const [regionsVisitedFilter, setRegionsVisitedFilter] = useState(generateFilter([]));
   const [countriesVisitedFilter, setCountriesVisitedFilter] = useState(generateFilter([]));
   const [dareVisitedFilter, setDareVisitedFilter] = useState(generateFilter([]));
+  const [seriesVisitedFilter, setSeriesVisitedFilter] = useState(generateFilter([]));
+  const [seriesNotVisitedFilter, setSeriesNotVisitedFilter] = useState(generateFilter([]));
   const [regionsVisited, setRegionsVisited] = useState<any[]>([]);
   const [countriesVisited, setCountriesVisited] = useState<any[]>([]);
   const [dareVisited, setDareVisited] = useState<any[]>([]);
+  const [seriesVisited, setSeriesVisited] = useState<any[]>([]);
+  const [images, setImages] = useState<any>({});
+  const { mutateAsync: updateSeriesItem } = usePostSetToggleItem();
+
+  useEffect(() => {
+    if (seriesIcons) {
+      let loadedImages: any = {};
+
+      seriesIcons.data.forEach(async (icon) => {
+        const id = icon.id;
+        const img = API_HOST + '/static/img/series_new2/' + icon.new_icon_png;
+        const imgVisited = API_HOST + '/static/img/series_new2/' + icon.new_icon_visited_png;
+        if (!img || !imgVisited) return;
+        try {
+          const iconImage = { uri: img };
+          const visitedIconImage = { uri: imgVisited };
+
+          loadedImages[id] = iconImage;
+          loadedImages[`${id}v`] = visitedIconImage;
+        } catch (error) {
+          console.error(`Error loading icon for series_id ${id}:`, error);
+        }
+      });
+
+      setImages(loadedImages);
+    }
+  }, [seriesIcons]);
 
   useEffect(() => {
     const loadDatabases = async () => {
@@ -362,9 +393,11 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
 
   useFocusEffect(
     useCallback(() => {
-      refetchVisitedRegions();
-      refetchVisitedCountries();
-      refetchVisitedDare();
+      if (token) {
+        refetchVisitedRegions();
+        refetchVisitedCountries();
+        refetchVisitedDare();
+      }
     }, [navigation])
   );
 
@@ -387,6 +420,12 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
     }
   }, [visitedDareIds]);
 
+  useEffect(() => {
+    if (visitedSeriesIds && token) {
+      setSeriesVisited(visitedSeriesIds.ids);
+    }
+  }, [visitedSeriesIds]);
+
   useEffect(() => {
     if (regionsVisited && regionsVisited.length) {
       setRegionsVisitedFilter(generateFilter(regionsVisited));
@@ -405,6 +444,37 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
     }
   }, [dareVisited]);
 
+  useEffect(() => {
+    if (!seriesFilter.visible) {
+      setSeriesVisitedFilter(generateFilter([]));
+      setSeriesNotVisitedFilter(generateFilter([]));
+      return;
+    }
+
+    if (seriesFilter.applied) {
+      if (seriesVisited?.length) {
+        setSeriesVisitedFilter([
+          'all',
+          ['any', ...seriesVisited.map((id) => ['==', 'id', id])],
+          ['any', ...seriesFilter.groups.map((groupId: number) => ['==', 'series_id', groupId])]
+        ]);
+        setSeriesNotVisitedFilter([
+          'all',
+          ['all', ...seriesVisited.map((id) => ['!=', 'id', id])],
+          ['any', ...seriesFilter.groups.map((groupId: number) => ['==', 'series_id', groupId])]
+        ]);
+      } else {
+        setSeriesNotVisitedFilter([
+          'any',
+          ...seriesFilter.groups.map((groupId: number) => ['==', 'series_id', groupId])
+        ]);
+      }
+    } else {
+      setSeriesVisitedFilter(['any', ...seriesVisited.map((id) => ['==', 'id', id])]);
+      setSeriesNotVisitedFilter(['all', ...seriesVisited.map((id) => ['!=', 'id', id])]);
+    }
+  }, [seriesVisited, seriesFilter]);
+
   useEffect(() => {
     if (route.params?.id && route.params?.type && db1 && db2 && db3) {
       handleFindRegion(route.params?.id, route.params?.type);
@@ -513,7 +583,10 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
 
   const onMapPress = async (event: any) => {
     if (!mapRef.current) return;
-    closeCallout();
+    if (selectedMarker) {
+      closeCallout();
+      return;
+    }
     try {
       const { screenPointX, screenPointY } = event.properties;
 
@@ -813,15 +886,26 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
     }
   };
 
-  // to do
-  const handleMarkerPress = async (event) => {
+  const handleMarkerPress = async (event: any) => {
     const { features } = event;
     if (features?.length) {
       const selectedFeature = features[0];
       const { coordinates } = selectedFeature.geometry;
-      const { name, icon, description, series_name } = selectedFeature.properties;
-
-      setSelectedMarker({ coordinates, name, icon, description, series_name });
+      const visited = seriesVisited.includes(selectedFeature.properties.id) ? 1 : 0;
+      const icon = images[selectedFeature.properties.series_id];
+
+      const { name, description, series_name, series_id, id } = selectedFeature.properties;
+
+      setSelectedMarker({
+        coordinates,
+        name,
+        icon,
+        description,
+        series_name,
+        visited,
+        series_id,
+        id
+      });
     }
   };
 
@@ -831,9 +915,33 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
 
   const toggleSeries = useCallback(
     async (item: any) => {
-      console.log('toggleSeries', item);
+      if (!token) {
+        setIsWarningModalVisible(true);
+        return;
+      }
+
+      const itemData = {
+        token,
+        series_id: item.series_id,
+        item_id: item.id,
+        checked: (item.visited === 0 ? 1 : 0) as 0 | 1,
+        double: 0 as 0 | 1
+      };
+
+      try {
+        updateSeriesItem(itemData);
+        if (item.visited === 1) {
+          setSeriesVisited((current) => current.filter((id) => id !== item.id));
+          setSelectedMarker((current: any) => ({ ...current, visited: 0 }));
+        } else {
+          setSeriesVisited((current) => [...current, item.id]);
+          setSelectedMarker((current: any) => ({ ...current, visited: 1 }));
+        }
+      } catch (error) {
+        console.error('Failed to update series state', error);
+      }
     },
-    [token]
+    [token, updateSeriesItem]
   );
 
   const handleModalStateChange = (updates: { [key: string]: any }) => {
@@ -853,6 +961,10 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
         onPress={onMapPress}
         onRegionDidChange={handleRegionDidChange}
       >
+        <MapLibreGL.Images images={images}>
+          <View />
+        </MapLibreGL.Images>
+
         {type === 'regions' && (
           <>
             <MapLibreGL.FillLayer
@@ -871,7 +983,7 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
               filter={regionsVisitedFilter as any}
               style={regions_visited.style}
               maxZoomLevel={regions_visited.maxzoom}
-              belowLayerID="series_layer_cluster_circle"
+              belowLayerID="waterway-name"
             />
           </>
         )}
@@ -893,7 +1005,7 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
               filter={countriesVisitedFilter as any}
               style={countries_visited.style}
               maxZoomLevel={countries_visited.maxzoom}
-              belowLayerID="series_layer_cluster_circle"
+              belowLayerID="waterway-name"
             />
           </>
         )}
@@ -915,7 +1027,7 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
               filter={dareVisitedFilter as any}
               style={dare_visited.style}
               maxZoomLevel={dare_visited.maxzoom}
-              belowLayerID="series_layer_cluster_circle"
+              belowLayerID="waterway-name"
             />
           </>
         )}
@@ -927,153 +1039,62 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
             filter={['==', 'id', selectedRegion]}
             style={selected_region.style}
             maxZoomLevel={selected_region.maxzoom}
-            belowLayerID="series_layer_cluster_circle"
+            belowLayerID="waterway-name"
           />
         )}
 
-        <MapLibreGL.VectorSource
-          id="nomadmania_series_cluster"
-          // to do
-          tileUrlTemplates={[
-            'https://maps.nomadmania.eu/tileserver/series_cluster/{z}/{x}/{y}.pbf'
-          ]}
-          onPress={(event) => console.log('series_cluster', event.features)}
-          minZoomLevel={0}
-          maxZoomLevel={24}
-        >
-          <MapLibreGL.CircleLayer
-            id="series_layer_cluster_circle"
-            sourceID={series_layer_cluster.source}
-            sourceLayerID={series_layer_cluster['source-layer']}
-            style={{
-              circleRadius: 14,
-              circleColor: '#FFFFFF',
-              circleStrokeColor: '#000000',
-              circleStrokeWidth: 2,
-              circleOpacity: 1
-            }}
-            maxZoomLevel={series_layer_cluster.maxzoom}
-          />
-          <MapLibreGL.SymbolLayer
-            id={series_layer_cluster.id}
-            sourceID={series_layer_cluster.source}
-            sourceLayerID={series_layer_cluster['source-layer']}
-            aboveLayerID="series_layer_cluster_circle"
-            style={{
-              textField: '{point_count}',
-              textFont: ['Noto Sans Bold'],
-              textSize: 12,
-              textColor: '#000000',
-              textHaloColor: '#FFFFFF',
-              textHaloWidth: 1,
-              textAnchor: 'center',
-              textOffset: [0, 0],
-              textAllowOverlap: true
-            }}
-            maxZoomLevel={series_layer_cluster.maxzoom}
-          />
-        </MapLibreGL.VectorSource>
         <MapLibreGL.VectorSource
           id="nomadmania_series"
-          tileUrlTemplates={['https://maps.nomadmania.eu/tileserver/series/{z}/{x}/{y}.pbf']}
+          tileUrlTemplates={[MAP_HOST + '/tileserver/series/{z}/{x}/{y}.pbf']}
           onPress={handleMarkerPress}
-          minZoomLevel={0}
-          maxZoomLevel={24}
         >
-          <MapLibreGL.CircleLayer
-            id="series_layer_circle"
-            sourceID={series_layer.source}
-            sourceLayerID={series_layer['source-layer']}
-            aboveLayerID="series_layer_cluster"
-            minZoomLevel={series_layer.minzoom}
-            maxZoomLevel={series_layer.maxzoom}
-            style={{
-              circleRadius: 14,
-              circleColor: '#FFFFFF',
-              circleStrokeWidth: 2,
-              circleStrokeColor: '#000000',
-              circleOpacity: 1
-            }}
-          />
-          <MapLibreGL.SymbolLayer
-            id={series_layer.id}
-            sourceID={series_layer.source}
-            sourceLayerID={series_layer['source-layer']}
-            aboveLayerID="series_layer_cluster"
-            style={{
-              iconImage: '{series_id}',
-              iconSize: 0.1,
-              textAnchor: 'top',
-              textField: '{series_name} - {name}',
-              textFont: ['Noto Sans Regular'],
-              textMaxWidth: 9,
-              textOffset: [0, 0.6],
-              textPadding: 2,
-              textSize: 12,
-              visibility: 'visible',
-              textOptional: true,
-              textIgnorePlacement: true,
-              iconColor: '#666',
-              iconOpacity: 1,
-              iconHaloColor: '#ffffff',
-              iconHaloWidth: 1,
-              iconHaloBlur: 0.5,
-              textHaloBlur: 0.5,
-              textHaloColor: '#ffffff',
-              textHaloWidth: 1,
-              textColor: '#666'
-            }}
-            minZoomLevel={series_layer.minzoom}
-            maxZoomLevel={series_layer.maxzoom}
-          />
+          {seriesFilter.status !== 1 ? (
+            <MapLibreGL.SymbolLayer
+              id={series_layer.id}
+              sourceID={series_layer.source}
+              sourceLayerID={series_layer['source-layer']}
+              belowLayerID={Platform.OS === 'android' ? 'waterway-name' : undefined}
+              filter={seriesNotVisitedFilter as any}
+              style={{
+                iconImage: ['get', 'series_id'],
+                iconSize: 0.18,
+                visibility: 'visible',
+                iconColor: '#666',
+                iconOpacity: 1,
+                iconHaloColor: '#ffffff',
+                iconHaloWidth: 1,
+                iconHaloBlur: 0.5
+              }}
+            />
+          ) : (
+            <></>
+          )}
+
+          {seriesFilter.status !== 0 ? (
+            <MapLibreGL.SymbolLayer
+              id={series_visited.id}
+              sourceID={series_visited.source}
+              sourceLayerID={series_visited['source-layer']}
+              belowLayerID={Platform.OS === 'android' ? 'waterway-name' : undefined}
+              filter={seriesVisitedFilter as any}
+              style={{
+                iconImage: '{series_id}v',
+                iconSize: 0.18,
+                visibility: 'visible',
+                iconColor: '#666',
+                iconOpacity: 1,
+                iconHaloColor: '#ffffff',
+                iconHaloWidth: 1,
+                iconHaloBlur: 0.5
+              }}
+            />
+          ) : (
+            <></>
+          )}
         </MapLibreGL.VectorSource>
 
         {selectedMarker && (
-          <MapLibreGL.PointAnnotation
-            id="selected_marker_callout"
-            coordinate={selectedMarker.coordinates}
-            anchor={{ x: 0.5, y: 1 }}
-          >
-            <View style={styles.customView}>
-              <View style={styles.calloutContainer}>
-                <View style={styles.calloutImageContainer}>
-                  <Image
-                    source={{ uri: selectedMarker.icon }}
-                    style={styles.calloutImage}
-                    resizeMode="contain"
-                  />
-                </View>
-                <View style={styles.calloutTextContainer}>
-                  <Text style={styles.seriesName}>{selectedMarker.series_name}</Text>
-                  <Text style={styles.markerName}>{selectedMarker.name}</Text>
-                </View>
-                <TouchableOpacity
-                  style={[
-                    styles.calloutButton,
-                    (selectedMarker.visited === 1 &&
-                      token && {
-                        backgroundColor: Colors.WHITE,
-                        borderWidth: 1,
-                        borderColor: Colors.BORDER_LIGHT
-                      }) ||
-                      {}
-                  ]}
-                  onPress={() => toggleSeries(selectedMarker)}
-                >
-                  {selectedMarker?.visited === 1 && token ? (
-                    <View style={styles.completedContainer}>
-                      <CheckSvg width={14} height={14} fill={Colors.DARK_BLUE} />
-                      <Text style={[styles.calloutButtonText, { color: Colors.DARK_BLUE }]}>
-                        Completed
-                      </Text>
-                    </View>
-                  ) : (
-                    <Text style={styles.calloutButtonText}>Mark Completed</Text>
-                  )}
-                </TouchableOpacity>
-              </View>
-            </View>
-          </MapLibreGL.PointAnnotation>
+          <MarkerItem marker={selectedMarker} toggleSeries={toggleSeries} token={token} />
         )}
         <MapLibreGL.Camera ref={cameraRef} />
         {location && (
@@ -1210,6 +1231,7 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
             style={[styles.cornerButton, styles.bottomButton, styles.bottomLeftButton]}
             onPress={() => {
               setIsFilterVisible(true);
+              closeCallout();
             }}
           >
             <FilterIcon />

+ 0 - 4
src/screens/InAppScreens/MapScreen/style.tsx

@@ -110,10 +110,6 @@ export const styles = StyleSheet.create({
     borderColor: Colors.TEXT_GRAY,
     marginTop: -34
   },
-  calloutImage: {
-    width: 28,
-    height: 28
-  },
   calloutTextContainer: {
     flex: 1,
     gap: 4,

+ 3 - 0
src/screens/InAppScreens/ProfileScreen/UsersMap/index.tsx

@@ -327,6 +327,7 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
               filter={regionsVisitedFilter as any}
               style={regions_visited.style}
               maxZoomLevel={regions_visited.maxzoom}
+              belowLayerID="waterway-name"
             />
           </>
         )}
@@ -348,6 +349,7 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
               filter={countriesVisitedFilter as any}
               style={countries_visited.style}
               maxZoomLevel={countries_visited.maxzoom}
+              belowLayerID="waterway-name"
             />
           </>
         )}
@@ -369,6 +371,7 @@ const UsersMapScreen: FC<Props> = ({ navigation, route }) => {
               filter={dareVisitedFilter as any}
               style={dare_visited.style}
               maxZoomLevel={dare_visited.maxzoom}
+              belowLayerID="waterway-name"
             />
           </>
         )}

+ 6 - 2
src/types/api.ts

@@ -149,7 +149,9 @@ export enum API_ENDPOINT {
   GET_LIST_REGIONS = 'get-list-regions',
   GET_LIST_COUNTRIES = 'get-list-countries',
   GET_LIST_DARE = 'get-list-dare',
-  GET_LATEST_VERSION = 'latest-version'
+  GET_LATEST_VERSION = 'latest-version',
+  GET_ICONS = 'get-icons',
+  GET_VISITED_SERIES_IDS = 'get-visited-series-ids'
 }
 
 export enum API {
@@ -273,7 +275,9 @@ export enum API {
   GET_LIST_REGIONS = `${API_ROUTE.REGIONS}/${API_ENDPOINT.GET_LIST_REGIONS}`,
   GET_LIST_COUNTRIES = `${API_ROUTE.COUNTRIES}/${API_ENDPOINT.GET_LIST_COUNTRIES}`,
   GET_LIST_DARE = `${API_ROUTE.DARE}/${API_ENDPOINT.GET_LIST_DARE}`,
-  LATEST_VERSION = `${API_ROUTE.APP}/${API_ENDPOINT.GET_LATEST_VERSION}`
+  LATEST_VERSION = `${API_ROUTE.APP}/${API_ENDPOINT.GET_LATEST_VERSION}`,
+  GET_ICONS = `${API_ROUTE.SERIES}/${API_ENDPOINT.GET_ICONS}`,
+  GET_VISITED_SERIES_IDS = `${API_ROUTE.MAPS}/${API_ENDPOINT.GET_VISITED_SERIES_IDS}`
 }
 
 export type BaseAxiosError = AxiosError;