Browse Source

users who ticked series

Viktoriia 1 day ago
parent
commit
a31f27aecb

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

@@ -10,3 +10,4 @@ export * from './use-post-get-suggestion-data';
 export * from './use-post-submit-suggestion';
 export * from './use-post-get-list';
 export * from './use-get-icons';
+export * from './use-post-get-users-who-ticked-series';

+ 28 - 0
src/modules/api/series/queries/use-post-get-users-who-ticked-series.tsx

@@ -0,0 +1,28 @@
+import { seriesQueryKeys } from '../series-query-keys';
+import { seriesApi } from '../series-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+import { useMutation } from '@tanstack/react-query';
+import { PostGetUsersWhoVisitedDataReturn } from '@api/regions';
+
+export const useGetUsersWhoTickesSeriesMutation = () => {
+  return useMutation<
+    PostGetUsersWhoVisitedDataReturn,
+    BaseAxiosError,
+    { id: number; page: number; sort?: string; age?: number; country?: string },
+    PostGetUsersWhoVisitedDataReturn
+  >({
+    mutationKey: seriesQueryKeys.getUsersWhoTickedSeries(),
+    mutationFn: async (variables) => {
+      const response = await seriesApi.getUsersWhoTickedSeries(
+        variables.id,
+        variables.page,
+        variables.sort,
+        variables.age,
+        variables.country
+      );
+      return response.data;
+    }
+  });
+};

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

@@ -1,6 +1,7 @@
 import { request } from '../../../utils';
 import { API } from '../../../types';
 import { ResponseType } from '../response-type';
+import { PostGetUsersWhoVisitedDataReturn } from '@api/regions';
 
 export interface PostGetSeries extends ResponseType {
   series: { id: number; name: string; icon: string }[];
@@ -197,5 +198,19 @@ export const seriesApi = {
   submitSuggestion: (data: SubmitSuggestionTypes) =>
     request.postForm<SubmitSuggestionReturn>(API.SUBMIT_SUGGESTION, data),
   getList: () => request.postForm<PostGetSeriesList>(API.GET_SERIES_LIST),
-  getIcons: () => request.postForm<PostGetSeriesIcons>(API.GET_ICONS)
+  getIcons: () => request.postForm<PostGetSeriesIcons>(API.GET_ICONS),
+  getUsersWhoTickedSeries: (
+    id: number,
+    page: number,
+    sort?: string,
+    age?: number,
+    country?: string
+  ) =>
+    request.postForm<PostGetUsersWhoVisitedDataReturn>(API.GET_USERS_WHO_TICKED_SERIES, {
+      id,
+      page,
+      sort,
+      age,
+      country
+    })
 };

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

@@ -13,5 +13,6 @@ export const seriesQueryKeys = {
   getSuggestionData: () => ['getSuggestionData'] as const,
   submitSuggestion: () => ['submitSuggestion'] as const,
   getList: () => ['getList'] as const,
-  getIcons: () => ['getIcons'] as const
+  getIcons: () => ['getIcons'] as const,
+  getUsersWhoTickedSeries: () => ['getUsersWhoTickedSeries'] as const
 };

+ 89 - 9
src/screens/InAppScreens/MapScreen/MarkerItem/index.tsx

@@ -6,17 +6,36 @@ import { Colors } from 'src/theme';
 
 import CheckSvg from 'assets/icons/mark.svg';
 import * as MapLibreRN from '@maplibre/maplibre-react-native';
+import { API_HOST } from 'src/constants';
+import { NAVIGATION_PAGES } from 'src/types';
+import { useNavigation } from '@react-navigation/native';
 
 const MarkerItem = ({
   marker,
   toggleSeries,
-  token
+  token,
+  isPremium,
+  setPremiumModalVisible
 }: {
   marker: any;
   toggleSeries: (item: any) => void;
   token: string;
+  isPremium: boolean;
+  setPremiumModalVisible: (premium: boolean) => void;
 }) => {
-  const calloutRef = useRef<MapLibreRN.PointAnnotationRef>(null);
+  const navigation = useNavigation();
+
+  function formatNumber(number: number) {
+    if (number >= 1000 && number < 10000) {
+      return (number / 1000).toFixed(1) + 'k';
+    } else if (number >= 10000) {
+      return (number / 1000).toFixed(0) + 'k';
+    }
+    return number.toString();
+  }
+  const formattedCount = formatNumber(marker.no_visited);
+  const parsedAvatars =
+    typeof marker.avatars === 'string' ? JSON.parse(marker.avatars) : marker.avatars;
 
   return (
     <>
@@ -41,6 +60,38 @@ const MarkerItem = ({
                   {marker.name}
                 </Text>
               </View>
+              {parsedAvatars && (
+                <TouchableOpacity
+                  onPress={() => {
+                    if (!isPremium) {
+                      setPremiumModalVisible(true);
+                    } else {
+                      navigation.navigate(
+                        ...([
+                          NAVIGATION_PAGES.USERS_LIST,
+                          {
+                            id: marker.id,
+                            isFromHere: false,
+                            type: 'series'
+                          }
+                        ] as never)
+                      );
+                    }
+                  }}
+                  style={styles.userImageContainer}
+                >
+                  {parsedAvatars?.map((avatar: number, index: number) => (
+                    <Image
+                      key={index}
+                      source={{ uri: API_HOST + '/img/avatars/' + avatar + '.webp' }}
+                      style={styles.userImageSmall}
+                    />
+                  ))}
+                  <View style={styles.userCountContainer}>
+                    <Text style={styles.userCount}>{formattedCount}</Text>
+                  </View>
+                </TouchableOpacity>
+              )}
               <TouchableOpacity
                 style={[
                   styles.calloutButton,
@@ -69,14 +120,11 @@ const MarkerItem = ({
           </View>
         </MapLibreRN.PointAnnotation>
       ) : (
-        <MapLibreRN.PointAnnotation
+        <MapLibreRN.MarkerView
           key={`${marker.id}-${marker.visited}`}
           id="selected_marker_callout"
           coordinate={marker.coordinates}
           anchor={{ x: 0.5, y: 0.9 }}
-          onSelected={() => toggleSeries(marker)}
-          selected={true}
-          ref={calloutRef}
         >
           <View style={styles.customView}>
             <View style={styles.calloutContainer}>
@@ -87,12 +135,44 @@ const MarkerItem = ({
                   resizeMode="contain"
                 />
               </View>
-              <View style={styles.calloutTextContainer}>
+              <View style={[styles.calloutTextContainer, { flex: 0 }]}>
                 <Text style={styles.seriesName}>{marker.series_name}</Text>
                 <Text style={styles.markerName} selectable={true}>
                   {marker.name}
                 </Text>
               </View>
+              {parsedAvatars && (
+                <TouchableOpacity
+                  onPressIn={() => {
+                    if (!isPremium) {
+                      setPremiumModalVisible(true);
+                    } else {
+                      navigation.navigate(
+                        ...([
+                          NAVIGATION_PAGES.USERS_LIST,
+                          {
+                            id: marker.id,
+                            isFromHere: false,
+                            type: 'series'
+                          }
+                        ] as never)
+                      );
+                    }
+                  }}
+                  style={styles.userImageContainer}
+                >
+                  {parsedAvatars?.map((avatar: number, index: number) => (
+                    <Image
+                      key={index}
+                      source={{ uri: API_HOST + '/img/avatars/' + avatar + '.webp' }}
+                      style={styles.userImageSmall}
+                    />
+                  ))}
+                  <View style={styles.userCountContainer}>
+                    <Text style={styles.userCount}>{formattedCount}</Text>
+                  </View>
+                </TouchableOpacity>
+              )}
               <TouchableOpacity
                 style={[
                   styles.calloutButton,
@@ -104,7 +184,7 @@ const MarkerItem = ({
                     }) ||
                     {}
                 ]}
-                onPress={() => toggleSeries(marker)}
+                onPressIn={() => toggleSeries(marker)}
               >
                 {marker?.visited === 1 && token ? (
                   <View style={styles.completedContainer}>
@@ -119,7 +199,7 @@ const MarkerItem = ({
               </TouchableOpacity>
             </View>
           </View>
-        </MapLibreRN.PointAnnotation>
+        </MapLibreRN.MarkerView>
       )}
     </>
   );

+ 28 - 0
src/screens/InAppScreens/MapScreen/MarkerItem/styles.tsx

@@ -100,5 +100,33 @@ export const styles = StyleSheet.create({
     shadowOpacity: 0.12,
     shadowRadius: 8,
     elevation: 5
+  },
+  userCountContainer: {
+    width: 28,
+    height: 28,
+    borderRadius: 14,
+    backgroundColor: Colors.DARK_LIGHT,
+    alignItems: 'center',
+    justifyContent: 'center',
+    marginLeft: -6
+  },
+  userCount: {
+    fontSize: 12,
+    color: Colors.DARK_BLUE,
+    lineHeight: 24
+  },
+  userImageContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    marginBottom: 12
+  },
+  userImageSmall: {
+    width: 28,
+    height: 28,
+    borderRadius: 14,
+    marginLeft: -6,
+    borderWidth: 1,
+    borderColor: Colors.DARK_LIGHT,
+    resizeMode: 'cover'
   }
 });

+ 130 - 30
src/screens/InAppScreens/MapScreen/MultipleSeriesModal/index.tsx

@@ -4,12 +4,28 @@ import ActionSheet, { SheetManager } from 'react-native-actions-sheet';
 import { Colors } from 'src/theme';
 import { getFontSize } from 'src/utils';
 import { FlashList } from '@shopify/flash-list';
+import { API_HOST } from 'src/constants';
+import { useNavigation } from '@react-navigation/native';
+import { NAVIGATION_PAGES } from 'src/types';
+import { useSubscription } from 'src/screens/OfflineMapsScreen/useSubscription';
 
 const MultipleSeriesModal = () => {
+  const navigation = useNavigation();
+  const { isPremium, loading } = useSubscription();
   const [seriesData, setSeriesData] = useState<any>(null);
   const [markers, setMarkers] = useState<any[]>([]);
 
   const shouldOpenWarningModalRef = useRef(false);
+  const shouldOpenPremiumWarningModalRef = useRef(false);
+
+  function formatNumber(number: number) {
+    if (number >= 1000 && number < 10000) {
+      return (number / 1000).toFixed(1) + 'k';
+    } else if (number >= 10000) {
+      return (number / 1000).toFixed(0) + 'k';
+    }
+    return number.toString();
+  }
 
   const handleSheetOpen = (payload: any) => {
     setSeriesData(payload);
@@ -56,6 +72,11 @@ const MultipleSeriesModal = () => {
 
           shouldOpenWarningModalRef.current = false;
           seriesData.setIsWarningModalVisible(true);
+        } else if (shouldOpenPremiumWarningModalRef.current) {
+          if (!seriesData) return;
+
+          shouldOpenPremiumWarningModalRef.current = false;
+          seriesData.setPremiumModalVisible(true);
         }
       }}
       containerStyle={styles.sheetContainer}
@@ -68,37 +89,90 @@ const MultipleSeriesModal = () => {
             keyExtractor={(item, index) => item.id.toString() + index.toString()}
             showsVerticalScrollIndicator={true}
             persistentScrollbar={true}
-            renderItem={({ item }) => (
-              <TouchableOpacity style={styles.option} onPress={() => handleItemPress(item)}>
-                <View style={styles.imageContainer}>
-                  <Image
-                    source={{ uri: item.icon?.uri || '' }}
-                    style={styles.icon}
-                    resizeMode="contain"
-                  />
-                  <View style={{ justifyContent: 'space-between', flex: 1 }}>
-                    <Text style={styles.name}>{item.series_name}</Text>
-                    <Text style={styles.description}>{item.name}</Text>
+            renderItem={({ item }) => {
+              const formattedCount = formatNumber(item.no_visited);
+              const parsedAvatars =
+                typeof item.avatars === 'string' ? JSON.parse(item.avatars) : item.avatars;
+
+              return (
+                <TouchableOpacity style={styles.option} onPress={() => handleItemPress(item)}>
+                  <View style={styles.imageContainer}>
+                    <Image
+                      source={{ uri: item.icon?.uri || '' }}
+                      style={styles.icon}
+                      resizeMode="contain"
+                    />
                   </View>
-                </View>
-
-                <TouchableOpacity
-                  onPress={() => handleToggleSeries(item)}
-                  style={[styles.markButton, item.visited === 1 && styles.visitedButton]}
-                >
-                  {item.visited === 1 ? (
-                    <View style={styles.completedContainer}>
-                      <Text style={[styles.calloutButtonText, { color: Colors.DARK_BLUE }]}>
-                        Completed
-                      </Text>
+
+                  <View style={{ flex: 1, gap: 8 }}>
+                    <View style={{ justifyContent: 'space-between', flex: 1 }}>
+                      <Text style={styles.name}>{item.series_name}</Text>
+                      <Text style={styles.description}>{item.name}</Text>
+                    </View>
+
+                    <View
+                      style={{
+                        flexDirection: 'row',
+                        justifyContent: 'space-between',
+                        alignItems: 'center',
+                        flex: 1
+                      }}
+                    >
+                      {parsedAvatars && (
+                        <TouchableOpacity
+                          onPress={() => {
+                            if (!isPremium) {
+                              shouldOpenPremiumWarningModalRef.current = true;
+                              SheetManager.hide('multiple-series-modal');
+
+                              return;
+                            }
+                            SheetManager.hide('multiple-series-modal');
+                            navigation.navigate(
+                              ...([
+                                NAVIGATION_PAGES.USERS_LIST,
+                                {
+                                  id: item.id,
+                                  isFromHere: false,
+                                  type: 'series'
+                                }
+                              ] as never)
+                            );
+                          }}
+                          style={styles.userImageContainer}
+                        >
+                          {parsedAvatars?.map((avatar: number, index: number) => (
+                            <Image
+                              key={index}
+                              source={{ uri: API_HOST + '/img/avatars/' + avatar + '.webp' }}
+                              style={styles.userImageSmall}
+                            />
+                          ))}
+                          <View style={styles.userCountContainer}>
+                            <Text style={styles.userCount}>{formattedCount}</Text>
+                          </View>
+                        </TouchableOpacity>
+                      )}
+
+                      <TouchableOpacity
+                        onPress={() => handleToggleSeries(item)}
+                        style={[styles.markButton, item.visited === 1 && styles.visitedButton]}
+                      >
+                        {item.visited === 1 ? (
+                          <View style={styles.completedContainer}>
+                            <Text style={[styles.calloutButtonText, { color: Colors.DARK_BLUE }]}>
+                              Completed
+                            </Text>
+                          </View>
+                        ) : (
+                          <Text style={styles.calloutButtonText}>To Complete</Text>
+                        )}
+                      </TouchableOpacity>
                     </View>
-                  ) : (
-                    <Text style={styles.calloutButtonText}>To Complete</Text>
-                  )}
+                  </View>
                 </TouchableOpacity>
-              </TouchableOpacity>
-            )}
-            estimatedItemSize={50}
+              );
+            }}
           />
         </View>
       )}
@@ -147,7 +221,6 @@ const styles = StyleSheet.create({
   option: {
     flexDirection: 'row',
     alignItems: 'center',
-    justifyContent: 'space-between',
     backgroundColor: Colors.FILL_LIGHT,
     paddingVertical: 8,
     paddingHorizontal: 12,
@@ -158,7 +231,6 @@ const styles = StyleSheet.create({
     flexDirection: 'row',
     alignItems: 'center',
     gap: 8,
-    flex: 1,
     marginRight: 8
   },
   name: {
@@ -172,6 +244,34 @@ const styles = StyleSheet.create({
     fontWeight: '600',
     color: Colors.DARK_BLUE,
     flex: 1
+  },
+  userCountContainer: {
+    width: 28,
+    height: 28,
+    borderRadius: 14,
+    backgroundColor: Colors.DARK_LIGHT,
+    alignItems: 'center',
+    justifyContent: 'center',
+    marginLeft: -6
+  },
+  userCount: {
+    fontSize: 12,
+    color: Colors.DARK_BLUE,
+    lineHeight: 24
+  },
+  userImageContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    marginLeft: 6
+  },
+  userImageSmall: {
+    width: 28,
+    height: 28,
+    borderRadius: 14,
+    marginLeft: -6,
+    borderWidth: 1,
+    borderColor: Colors.DARK_LIGHT,
+    resizeMode: 'cover'
   }
 });
 

+ 39 - 1
src/screens/InAppScreens/MapScreen/UsersListScreen/index.tsx

@@ -21,6 +21,7 @@ import {
   useGetUsersFromCountryMutation,
   useGetUsersWhoVisitedCountryMutation
 } from '@api/countries';
+import { useGetUsersWhoTickesSeriesMutation } from '@api/series';
 
 type Props = {
   navigation: NavigationProp<any>;
@@ -36,6 +37,7 @@ const UsersListScreen: FC<Props> = ({ navigation, route }) => {
   const { mutateAsync: getUsersFromCountry } = useGetUsersFromCountryMutation();
   const { mutateAsync: getUsersWhoVisitedCountry } = useGetUsersWhoVisitedCountryMutation();
   const { mutateAsync: getFriends } = useGetFriendsMutation();
+  const { mutateAsync: getUsersWhoTickedSeries } = useGetUsersWhoTickesSeriesMutation();
   const [users, setUsers] = useState<any[]>([]);
   const [loading, setLoading] = useState(true);
   const [selectedUsers, setSelectedUsers] = useState<any[]>([]);
@@ -146,6 +148,23 @@ const UsersListScreen: FC<Props> = ({ navigation, route }) => {
             }
           }
         );
+      } else if (type === 'series') {
+        await getUsersWhoTickedSeries(
+          {
+            id,
+            page,
+            sort: filter.ranking,
+            age: filter.age,
+            country: filter.country
+          },
+          {
+            onSuccess: (data) => {
+              setIsLoadingMore(false);
+              setUsers((prevState) => [...prevState, ...data?.data?.users]);
+              setSelectedUsers((prevState) => [...prevState, ...data?.data?.users]);
+            }
+          }
+        );
       } else {
         await getUsersWhoVisitedDare(
           {
@@ -264,6 +283,26 @@ const UsersListScreen: FC<Props> = ({ navigation, route }) => {
           }
         }
       );
+    } else if (type === 'series') {
+      await getUsersWhoTickedSeries(
+        {
+          id,
+          page,
+          sort: filter.ranking,
+          age: filter.age,
+          country: filter.country
+        },
+        {
+          onSuccess: (data) => {
+            setUsers(data?.data?.users);
+            setSelectedUsers(data?.data?.users);
+            setMaxPages(data?.data?.max_pages);
+            !masterCountries?.length &&
+              setMasterCountries(convertData(data?.data?.countries) ?? []);
+            setLoading(false);
+          }
+        }
+      );
     } else {
       await getUsersWhoVisitedDare(
         {
@@ -325,7 +364,6 @@ const UsersListScreen: FC<Props> = ({ navigation, route }) => {
           itemVisiblePercentThreshold: 50,
           minimumViewTime: 1000
         }}
-        estimatedItemSize={50}
         data={filteredUsers}
         renderItem={({ item, index }) => (
           <Profile

+ 28 - 5
src/screens/InAppScreens/MapScreen/index.tsx

@@ -390,6 +390,7 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
   const [isEditModalVisible, setIsEditModalVisible] = useState(false);
   const [isFilterVisible, setIsFilterVisible] = useState<string | null>(null);
   const [isLocationLoading, setIsLocationLoading] = useState(false);
+  const [premiumModalVisible, setPremiumModalVisible] = useState(false);
 
   const [modalState, setModalState] = useState({
     selectedFirstYear: 2021,
@@ -1490,7 +1491,9 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
               series_name: f.properties.series_name,
               visited: seriesVisited.includes(f.properties.id) ? 1 : 0,
               series_id: f.properties.series_id,
-              id: f.properties.id
+              id: f.properties.id,
+              avatars: f.properties.avatars,
+              no_visited: f.properties.no_visited
             };
           })
           .sort((a: any, b: any) => a.visited - b.visited);
@@ -1501,7 +1504,8 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
             token,
             toggleSeries,
             setSelectedMarker,
-            setIsWarningModalVisible
+            setIsWarningModalVisible,
+            setPremiumModalVisible
           } as any
         });
 
@@ -1514,7 +1518,8 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
       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;
+      const { name, description, series_name, series_id, id, avatars, no_visited } =
+        selectedFeature.properties;
 
       if (coordinates) {
         setSelectedMarker({
@@ -1525,7 +1530,9 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
           series_name,
           visited,
           series_id,
-          id
+          id,
+          avatars,
+          no_visited
         });
         setSelectedUser(null);
       }
@@ -1983,7 +1990,13 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
         {selectedUser && <UserItem marker={selectedUser} />}
 
         {selectedMarker && (
-          <MarkerItem marker={selectedMarker} toggleSeries={toggleSeries} token={token} />
+          <MarkerItem
+            marker={selectedMarker}
+            toggleSeries={toggleSeries}
+            token={token}
+            isPremium={isPremium}
+            setPremiumModalVisible={setPremiumModalVisible}
+          />
         )}
         {(renderCamera || Platform.OS === 'ios') && (
           <MapLibreRN.Camera ref={cameraRef} followUserLocation={undefined} animationMode="flyTo" />
@@ -2315,6 +2328,16 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
         }}
         message="NomadMania app needs location permissions to function properly. Open settings?"
       />
+      <WarningModal
+        type={'success'}
+        isVisible={premiumModalVisible}
+        message={
+          'This feature is available to Premium users. Premium account settings can be managed on our website.'
+        }
+        action={() => {}}
+        onClose={() => setPremiumModalVisible(false)}
+        title={'Premium Feature'}
+      />
       <MultipleSeriesModal />
     </SafeAreaView>
   );

+ 4 - 2
src/types/api.ts

@@ -223,7 +223,8 @@ export enum API_ENDPOINT {
   GET_SINGLE_REGION = 'get-single-region',
   SET_NOT_VISITED = 'set-not-visited',
   GET_GROUP_CONVERSATION_ALL = 'get-group-conversation-all',
-  GET_CONVERSATION_WITH_ALL = 'get-conversation-with-all'
+  GET_CONVERSATION_WITH_ALL = 'get-conversation-with-all',
+  GET_USERS_WHO_TICKED_SERIES = 'get-users-who-ticked-series'
 }
 
 export enum API {
@@ -420,7 +421,8 @@ export enum API {
   GET_SINGLE_REGION = `${API_ROUTE.QUICK_ENTER}/${API_ENDPOINT.GET_SINGLE_REGION}`,
   SET_NOT_VISITED = `${API_ROUTE.QUICK_ENTER}/${API_ENDPOINT.SET_NOT_VISITED}`,
   GET_GROUP_CONVERSATION_ALL = `${API_ROUTE.CHAT}/${API_ENDPOINT.GET_GROUP_CONVERSATION_ALL}`,
-  GET_CONVERSATION_WITH_ALL = `${API_ROUTE.CHAT}/${API_ENDPOINT.GET_CONVERSATION_WITH_ALL}`
+  GET_CONVERSATION_WITH_ALL = `${API_ROUTE.CHAT}/${API_ENDPOINT.GET_CONVERSATION_WITH_ALL}`,
+  GET_USERS_WHO_TICKED_SERIES = `${API_ROUTE.SERIES}/${API_ENDPOINT.GET_USERS_WHO_TICKED_SERIES}`
 }
 
 export type BaseAxiosError = AxiosError;