Browse Source

Merge branch 'series-ranking' of SashaGoncharov19/nomadmania-app into dev

Viktoriia 1 year ago
parent
commit
fb371c8cb3

+ 5 - 4
Route.tsx

@@ -29,6 +29,8 @@ import UNMastersScreen from './src/screens/InAppScreens/TravellersScreen/UNMaste
 import StatisticsScreen from './src/screens/InAppScreens/TravellersScreen/StatisticsScreen';
 import StatisticsListScreen from './src/screens/InAppScreens/TravellersScreen/StatisticsListScreen';
 import TriumphsScreen from 'src/screens/InAppScreens/TravellersScreen/TriumphsScreen';
+import SeriesRankingScreen from 'src/screens/InAppScreens/TravellersScreen/SeriesRankingScreen';
+import SeriesRankingListScreen from 'src/screens/InAppScreens/TravellersScreen/SeriesRankingListScreen';
 
 import SeriesScreen from 'src/screens/InAppScreens/TravelsScreen/Series';
 import { SeriesItemScreen } from 'src/screens/InAppScreens/TravelsScreen/SeriesItemScreen';
@@ -164,10 +166,9 @@ const Route = () => {
                     name={NAVIGATION_PAGES.STATISTICS_LIST_DATA}
                     component={StatisticsListScreen}
                   />
-                  <ScreenStack.Screen
-                    name={NAVIGATION_PAGES.TRIUMPHS}
-                    component={TriumphsScreen}
-                  />
+                  <ScreenStack.Screen name={NAVIGATION_PAGES.TRIUMPHS} component={TriumphsScreen} />
+                  <ScreenStack.Screen name={NAVIGATION_PAGES.SERIES_RANKING} component={SeriesRankingScreen} />
+                  <ScreenStack.Screen name={NAVIGATION_PAGES.SERIES_RANKING_LIST} component={SeriesRankingListScreen} />
                 </ScreenStack.Navigator>
               )}
             </BottomTab.Screen>

+ 3 - 0
assets/icons/arrow-bold.svg

@@ -0,0 +1,3 @@
+<svg width="10" height="6" viewBox="0 0 10 6" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M9 4.79212L5.70707 1.49919C5.31818 1.1103 4.68182 1.1103 4.29293 1.49919L1 4.79212" stroke="#ED9334" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 2 - 1
package.json

@@ -59,7 +59,8 @@
     "react-native-svg": "13.9.0",
     "react-native-tab-view": "^3.5.2",
     "yup": "^1.3.3",
-    "zustand": "^4.4.7"
+    "zustand": "^4.4.7",
+    "@shopify/flash-list": "1.4.3"
   },
   "devDependencies": {
     "@babel/core": "^7.20.0",

+ 4 - 1
src/components/HorizontalTabView/index.tsx

@@ -13,7 +13,8 @@ export const HorizontalTabView = ({
   routes,
   renderScene,
   withMark,
-  onDoubleClick
+  onDoubleClick,
+  lazy = false
 }: {
   index: number;
   setIndex: React.Dispatch<React.SetStateAction<number>>;
@@ -21,6 +22,7 @@ export const HorizontalTabView = ({
   renderScene: (props: any) => React.ReactNode;
   withMark?: boolean;
   onDoubleClick?: () => void;
+  lazy?: boolean;
 }) => {
   const renderTabBar = (props: any) => (
     <TabBar
@@ -57,6 +59,7 @@ export const HorizontalTabView = ({
       swipeEnabled={true}
       style={styles.tabView}
       renderTabBar={renderTabBar}
+      lazy={lazy}
     />
   );
 };

+ 2 - 0
src/database/index.ts

@@ -8,6 +8,7 @@ import { fetchAndSaveAllTypesAndMasters } from './unMastersService';
 import { updateAvatars } from './avatarsService';
 import { fetchAndSaveStatistics } from './statisticsService';
 import { saveTriumphsData } from './triumphsService';
+import { saveSeriesRankingData } from './seriesRankingService';
 
 const db = SQLite.openDatabase('nomadManiaDb.db');
 const lastUpdateDate = storage.get('lastUpdateDate', StoreType.STRING) as string || '1990-01-01';
@@ -88,6 +89,7 @@ const updateMasterRanking = async () => {
   await fetchAndSaveStatistics(token);
   await fetchAndSaveAllTypesAndMasters();
   await saveTriumphsData();
+  await saveSeriesRankingData();
 };
 
 export default setupDatabaseAndSync;

+ 30 - 0
src/database/seriesRankingService/index.ts

@@ -0,0 +1,30 @@
+import { seriesApi } from '@api/series';
+import { storage } from 'src/storage';
+
+const NAMESPACE = 'series';
+
+function saveData<T>(key: string, data: T) {
+  const namespacedKey = `${NAMESPACE}:${key}`;
+  const jsonData = JSON.stringify(data);
+  storage.set(namespacedKey, jsonData);
+}
+
+export async function saveSeriesRankingData() {
+  const response = await seriesApi.getSeriesGroupsRanking();
+  const groups = response.data.data;
+  saveData('groups', groups);
+
+  await Promise.all(
+    groups.map(async (group) => {
+      const response = await seriesApi.getSeriesRanking(group.id, 0, 50);
+      saveData(`${group.id}`, response.data.data);
+
+      if (group.series) {
+        group.series.map(async (series) => {
+          const subseries = await seriesApi.getSeriesRanking(series.id, 0, 50);
+          saveData(`${series.id}`, subseries.data.data);
+        });
+      }
+    })
+  );
+}

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

@@ -3,3 +3,5 @@ export * from './use-post-get-series-groups';
 export * from './use-post-get-series-with-group';
 export * from './use-post-get-items-for-series';
 export * from './use-post-get-toggle-item';
+export * from './use-post-get-series-groups-ranking';
+export * from './use-post-get-series-ranking';

+ 29 - 0
src/modules/api/series/queries/use-post-get-series-groups-ranking.tsx

@@ -0,0 +1,29 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { seriesQueryKeys } from '../series-query-keys';
+import { seriesApi, type PostGetSeriesGroupsRanking, type GroupsData } from '../series-api';
+
+import type { BaseAxiosError } from '../../../../types';
+import { useConnection } from 'src/contexts/ConnectionContext';
+import { loadData } from 'src/storage';
+
+export const useGetSeriesGroupsRanking = (enabled: boolean) => {
+  const netInfo = useConnection();
+
+  return useQuery<PostGetSeriesGroupsRanking, BaseAxiosError>({
+    queryKey: seriesQueryKeys.getSeriesGroupsRanking(),
+    queryFn: async () => {
+      if (!netInfo?.isInternetReachable) {
+        const localData = loadData<GroupsData[]>('series', `groups`);
+        if (localData) {
+          return { data: localData } as PostGetSeriesGroupsRanking;
+        }
+        return { data: [], result: 'ERROR' } as PostGetSeriesGroupsRanking;
+      }
+
+      const response = await seriesApi.getSeriesGroupsRanking();
+      return response.data;
+    },
+    enabled
+  });
+};

+ 34 - 0
src/modules/api/series/queries/use-post-get-series-ranking.tsx

@@ -0,0 +1,34 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { seriesQueryKeys } from '../series-query-keys';
+import { seriesApi, type PostGetSeriesRanking, type RankingData } from '../series-api';
+
+import type { BaseAxiosError } from '../../../../types';
+import { useConnection } from 'src/contexts/ConnectionContext';
+import { loadData } from 'src/storage';
+
+export const useGetSeriesRanking = (
+  id: number,
+  page: number,
+  page_size: number,
+  enabled: boolean
+) => {
+  const netInfo = useConnection();
+
+  return useQuery<PostGetSeriesRanking, BaseAxiosError>({
+    queryKey: seriesQueryKeys.getSeriesRanking(id, page, page_size),
+    queryFn: async () => {
+      if (!netInfo?.isInternetReachable) {
+        const localData = loadData<RankingData>('series', `${id}`);
+        if (localData) {
+          return { data: localData, result: 'OK' } as PostGetSeriesRanking;
+        }
+        return { data: {}, result: 'ERROR' } as PostGetSeriesRanking;
+      }
+
+      const response = await seriesApi.getSeriesRanking(id, page, page_size);
+      return response.data;
+    },
+    enabled
+  });
+};

+ 75 - 17
src/modules/api/series/series-api.tsx

@@ -32,7 +32,7 @@ export interface PostGetSeriesList extends ResponseType {
     score: number;
     icon_png: string;
     count2: number;
-  }[]
+  }[];
 }
 
 export interface PostGetItems extends ResponseType {
@@ -42,18 +42,18 @@ export interface PostGetItems extends ResponseType {
     series_id: number;
     series_name: string;
     items: {
-      item_id: number,
-      icon: string | null,
-      name: string,
-      readonly: boolean,
-      info: string,
-      new: boolean,
-      checked: boolean,
-      checked_double: boolean,
-      series_id: number,
-      double: boolean
-    }[]
-  }[]
+      item_id: number;
+      icon: string | null;
+      name: string;
+      readonly: boolean;
+      info: string;
+      new: boolean;
+      checked: boolean;
+      checked_double: boolean;
+      series_id: number;
+      double: boolean;
+    }[];
+  }[];
 }
 
 export interface PostSetToggleItemReturn extends ResponseType {
@@ -68,15 +68,73 @@ export interface PostSetToggleItem {
   double: 0 | 1;
 }
 
+export interface GroupsData {
+  name: string;
+  id: number;
+  series:
+    | {
+        id: number;
+        name: string;
+      }[]
+    | null;
+}
+
+export interface PostGetSeriesGroupsRanking extends ResponseType {
+  data: GroupsData[];
+}
+
+export interface RankingData {
+  max_points: number;
+  max_pages: number;
+  ranking: {
+    user_id: number;
+    score: number;
+    avatar: string | null;
+    first_name: string;
+    last_name: string;
+    age: number;
+    authenticated: 0 | 1;
+    badge_nm: 0 | 1;
+    badge_un: 0 | 1;
+    flag1: string;
+    flag2: string | null;
+    rip: boolean;
+    recent_score_increase: boolean;
+  }[];
+}
+
+export interface PostGetSeriesRanking extends ResponseType {
+  data: RankingData;
+}
+
 export const seriesApi = {
   getSeries: (token: string | null, regions: string) =>
     request.postForm<PostGetSeries>(API.SERIES, { token, regions }),
-  getSeriesGroups: () =>
-    request.postForm<PostGetSeriesGroups>(API.SERIES_GROUPS),
+  getSeriesGroups: () => request.postForm<PostGetSeriesGroups>(API.SERIES_GROUPS),
   getSeriesWithGroup: (token: string, group: string) =>
     request.postForm<PostGetSeriesList>(API.SERIES_WITH_GROUP, { token, group }),
   getItemsForSeries: (token: string, series_id: string) =>
     request.postForm<PostGetItems>(API.GET_ITEMS_FOR_SERIES, { token, series_id }),
-  setToggleItem: (token: string, series_id: number, item_id: number, checked: 0 | 1, double: 0 | 1) =>
-    request.postForm<PostSetToggleItemReturn>(API.TOGGLE_ITEM_SERIES, { token, series_id, item_id, checked, double}),
+  setToggleItem: (
+    token: string,
+    series_id: number,
+    item_id: number,
+    checked: 0 | 1,
+    double: 0 | 1
+  ) =>
+    request.postForm<PostSetToggleItemReturn>(API.TOGGLE_ITEM_SERIES, {
+      token,
+      series_id,
+      item_id,
+      checked,
+      double
+    }),
+  getSeriesGroupsRanking: () =>
+    request.postForm<PostGetSeriesGroupsRanking>(API.GET_SERIES_GROUPS_RANKING),
+  getSeriesRanking: (id: number, page: number, page_size: number) =>
+    request.postForm<PostGetSeriesRanking>(API.GET_SERIES_RANKING, {
+      id,
+      page,
+      page_size
+    })
 };

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

@@ -4,4 +4,6 @@ export const seriesQueryKeys = {
   getSeriesWithGroup: () => ['getSeriesWithGroup'] 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
 };

+ 113 - 0
src/screens/InAppScreens/TravellersScreen/Components/SeriesRankingItem.tsx

@@ -0,0 +1,113 @@
+import React from 'react';
+import { Text, TouchableOpacity, View, Image } from 'react-native';
+import * as FileSystem from 'expo-file-system';
+
+import { AvatarWithInitials } from '../../../../components';
+
+import { API_HOST } from 'src/constants';
+import { getFontSize } from 'src/utils';
+import { Colors } from 'src/theme';
+import { SeriesRanking } from '../utils/types';
+import { ProfileStyles, ScoreStyles } from './styles';
+import { useConnection } from 'src/contexts/ConnectionContext';
+
+import TickIcon from 'assets/icons/tick.svg';
+import UNIcon from 'assets/icons/un_icon.svg';
+import NMIcon from 'assets/icons/nm_icon.svg';
+import ArrowIcon from 'assets/icons/arrow-bold.svg';
+
+const SeriesRankingItem = React.memo(
+  ({
+    item,
+    index,
+    onPress
+  }: {
+    item: SeriesRanking;
+    index: number;
+    onPress: (userId: number) => void;
+  }) => {
+    const netInfo = useConnection();
+
+    const avatarBaseUri = netInfo?.isInternetReachable
+      ? API_HOST + item.avatar
+      : `${FileSystem.documentDirectory}avatars/${item.user_id}.webp`;
+
+    const flagBaseUri = (flag: string) =>
+      netInfo?.isInternetReachable
+        ? API_HOST + flag
+        : `${FileSystem.documentDirectory}flags/${flag.split('/').pop()}`;
+
+    return (
+      <TouchableOpacity
+        onPress={() => onPress(item.user_id)}
+        style={ProfileStyles.profileContainer}
+      >
+        <View style={ProfileStyles.profileRoot}>
+          <View style={{ paddingLeft: 20 }}>
+            <View>
+              <Text style={ScoreStyles.rankText}>{index + 1}</Text>
+            </View>
+            <View style={{ marginLeft: index + 1 < 100 ? 5 : index + 1 < 1000 ? 15 : 25 }}>
+              {item.avatar ? (
+                <Image style={ProfileStyles.profileAvatar} source={{ uri: avatarBaseUri }} />
+              ) : (
+                <AvatarWithInitials
+                  text={`${item.first_name[0] ?? ''}${item.last_name[0] ?? ''}`}
+                  flag={flagBaseUri(item.flag1)}
+                  size={48}
+                />
+              )}
+            </View>
+          </View>
+          <View style={{ gap: 5, flex: 1 }}>
+            <Text
+              style={[ProfileStyles.profileFirstLastName, { fontSize: getFontSize(14), flex: 0 }]}
+            >
+              {item.first_name ?? ''} {item.last_name ?? ''}
+            </Text>
+            <View style={ProfileStyles.profileDataContainer}>
+              <View style={ProfileStyles.profileDataWrapper}>
+                <Text style={ProfileStyles.profileAge}>Age: {item.age ?? ''}</Text>
+                <Image
+                  source={{ uri: flagBaseUri(item.flag1) }}
+                  style={ProfileStyles.countryFlag}
+                />
+                {item.flag2 && item.flag2 !== item.flag1 ? (
+                  <Image
+                    source={{ uri: flagBaseUri(item.flag2) }}
+                    style={[ProfileStyles.countryFlag, { marginLeft: -15 }]}
+                  />
+                ) : null}
+                <View style={ProfileStyles.badgesWrapper}>
+                  {item.authenticated === 1 ? <TickIcon /> : null}
+                  {item.badge_un === 1 ? <UNIcon /> : null}
+                  {item.badge_nm === 1 ? <NMIcon /> : null}
+                </View>
+              </View>
+
+              <View style={ProfileStyles.rightScore}>
+                <Text
+                  style={[
+                    { fontWeight: 'bold', fontSize: 16 },
+                    item.recent_score_increase
+                      ? { color: Colors.ORANGE }
+                      : { color: Colors.DARK_BLUE }
+                  ]}
+                >
+                  {item.score}
+                </Text>
+                {item.recent_score_increase ? (
+                  <ArrowIcon stroke={Colors.ORANGE} />
+                ) : (
+                  <View style={{ width: 10 }}></View>
+                )}
+              </View>
+            </View>
+          </View>
+        </View>
+      </TouchableOpacity>
+    );
+  }
+);
+
+export default SeriesRankingItem;

+ 8 - 1
src/screens/InAppScreens/TravellersScreen/Components/styles.ts

@@ -61,7 +61,14 @@ export const ProfileStyles = StyleSheet.create({
     borderRadius: 20 / 2,
     borderWidth: 0.5,
     borderColor: 'gray'
-  }
+  },
+  profileContainer: {
+    justifyContent: 'space-between',
+    flexDirection: 'row',
+    alignItems: 'center',
+    marginBottom: 16
+  },
+  rightScore: { flexDirection: 'row', alignItems: 'center', gap: 4 }
 });
 
 export const ScoreStyles = StyleSheet.create({

+ 145 - 0
src/screens/InAppScreens/TravellersScreen/SeriesRankingListScreen/index.tsx

@@ -0,0 +1,145 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import { ActivityIndicator } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+import { FlashList } from '@shopify/flash-list';
+
+import {
+  Header,
+  HorizontalTabView,
+  Loading,
+  PageWrapper,
+  WarningModal
+} from '../../../../components';
+import SeriesRankingItem from '../Components/SeriesRankingItem';
+
+import { useGetSeriesRanking } from '@api/series';
+import { useConnection } from 'src/contexts/ConnectionContext';
+import { NAVIGATION_PAGES } from 'src/types';
+import { StoreType, storage } from 'src/storage';
+import { Colors } from 'src/theme';
+import { SeriesRanking } from '../utils/types';
+
+const SeriesRankingListScreen = ({ route }: { route: any }) => {
+  const name = route.params.name;
+  const id = route.params.id;
+  const series = route.params.series;
+  const [index, setIndex] = useState(0);
+  const [routes, setRoutes] = useState<{ key: string; title: string }[]>([]);
+  const [loading, setLoading] = useState(true);
+  const [seriesId, setSeriesId] = useState(id);
+  const [page, setPage] = useState(0);
+  const { data } = useGetSeriesRanking(seriesId, page, 50, true);
+  const [allData, setAllData] = useState<SeriesRanking[]>([]);
+  const netInfo = useConnection();
+  const navigation = useNavigation();
+  const token = storage.get('token', StoreType.STRING);
+  const [modalType, setModalType] = useState<string | null>(null);
+  const [isLoadingMore, setIsLoadingMore] = useState(false);
+
+  useEffect(() => {
+    if (series) {
+      const parsedRoutes = series.map((item: { id: string; name: string }) => ({
+        key: item.id,
+        title: item.name
+      }));
+
+      setRoutes([{ key: id, title: '  All  ' }, ...(parsedRoutes || [])]);
+    }
+    setLoading(false);
+  }, [series]);
+
+  useEffect(() => {
+    if (data && data.result == 'OK') {
+      if (page === 0) {
+        setAllData(data.data.ranking);
+        setIsLoadingMore(false);
+        return;
+      }
+
+      setAllData((prevData) => [...prevData, ...data.data.ranking]);
+      setIsLoadingMore(false);
+    }
+  }, [data]);
+
+  useEffect(() => {
+    if (routes[index]) {
+      setSeriesId(routes[index].key);
+      setPage(0);
+    }
+  }, [index]);
+
+  const handlePress = useCallback(
+    (userId: number) => {
+      if (!netInfo?.isInternetReachable) {
+        setModalType('offline');
+      } else if (!token) {
+        setModalType('unauthorized');
+      } else {
+        navigation.navigate(...([NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW, { userId }] as never));
+      }
+    },
+    [netInfo, token, navigation]
+  );
+
+  const handleEndReached = useCallback(() => {
+    if (
+      data &&
+      data.result === 'OK' &&
+      page < data.data.max_pages &&
+      netInfo?.isInternetReachable
+    ) {
+      setIsLoadingMore(true);
+      setPage((prevPage) => prevPage + 1);
+    }
+  }, [data, page, netInfo?.isInternetReachable]);
+
+  if (loading) return <Loading />;
+
+  const ListFooter = ({ isLoading }: { isLoading: boolean }) =>
+    isLoading ? <ActivityIndicator size="large" color={Colors.DARK_BLUE} /> : null;
+
+  const renderSeriesList = () => (
+    <FlashList
+      viewabilityConfig={{
+        waitForInteraction: true,
+        itemVisiblePercentThreshold: 50,
+        minimumViewTime: 1000
+      }}
+      estimatedItemSize={50}
+      contentContainerStyle={{ paddingVertical: 16 }}
+      data={allData}
+      showsVerticalScrollIndicator={false}
+      keyExtractor={(item, index) => index.toString()}
+      renderItem={({ item, index }: { item: SeriesRanking; index: number }) => (
+        <SeriesRankingItem item={item} index={index} onPress={handlePress} />
+      )}
+      onEndReached={handleEndReached}
+      onEndReachedThreshold={0.1}
+      ListFooterComponent={<ListFooter isLoading={isLoadingMore} />}
+    />
+  );
+
+  return (
+    <PageWrapper style={{ flex: 1 }}>
+      <Header label={name} />
+      {series ? (
+        <HorizontalTabView
+          index={index}
+          setIndex={setIndex}
+          routes={routes}
+          renderScene={({ route }: { route: { key: string; title: string } }) =>
+            allData && allData.length ? renderSeriesList() : <Loading />
+          }
+          lazy={true}
+        />
+      ) : allData && allData.length ? (
+        renderSeriesList()
+      ) : null}
+      {modalType && (
+        <WarningModal type={modalType} isVisible={true} onClose={() => setModalType(null)} />
+      )}
+    </PageWrapper>
+  );
+};
+
+export default SeriesRankingListScreen;

+ 51 - 0
src/screens/InAppScreens/TravellersScreen/SeriesRankingScreen/index.tsx

@@ -0,0 +1,51 @@
+import React from 'react';
+import { ScrollView, Text, TouchableOpacity, View } from 'react-native';
+
+import { Header, PageWrapper } from 'src/components';
+
+import { useNavigation } from '@react-navigation/native';
+import { useGetSeriesGroupsRanking } from '@api/series';
+import { styles } from './styles';
+import { NAVIGATION_PAGES } from 'src/types';
+
+const SeriesRankingScreen = () => {
+  const navigation = useNavigation();
+  const { data: groups } = useGetSeriesGroupsRanking(true);
+
+  return (
+    <PageWrapper>
+      <Header label="Series Ranking" />
+      <ScrollView showsVerticalScrollIndicator={false}>
+        <View style={{ gap: 8 }}>
+          {groups?.data.map((route, i) => {
+            return (
+              <View key={i}>
+                <TouchableOpacity
+                  style={styles.itemContainer}
+                  onPress={() =>
+                    navigation.navigate(
+                      ...([
+                        NAVIGATION_PAGES.SERIES_RANKING_LIST,
+                        {
+                          name: route.name,
+                          id: route.id,
+                          series: route.series
+                        }
+                      ] as never)
+                    )
+                  }
+                >
+                  <View>
+                    <Text style={styles.title}>{route.name}</Text>
+                  </View>
+                </TouchableOpacity>
+              </View>
+            );
+          })}
+        </View>
+      </ScrollView>
+    </PageWrapper>
+  );
+};
+
+export default SeriesRankingScreen;

+ 18 - 0
src/screens/InAppScreens/TravellersScreen/SeriesRankingScreen/styles.tsx

@@ -0,0 +1,18 @@
+import { StyleSheet } from 'react-native';
+import { getFontSize } from 'src/utils';
+
+export const styles = StyleSheet.create({
+  itemContainer: {
+    display: 'flex',
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+    paddingVertical: 8,
+    paddingRight: 12
+  },
+  title: {
+    fontSize: getFontSize(16),
+    color: 'rgba(237, 147, 52, 1)',
+    fontWeight: '700'
+  },
+});

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

@@ -40,7 +40,10 @@ const buttons: MenuButtonType[] = [
   },
   {
     label: 'Series Ranking',
-    icon: <StreetPeopleIcon fill={Colors.DARK_BLUE} width={20} height={20} />
+    icon: <StreetPeopleIcon fill={Colors.DARK_BLUE} width={20} height={20} />,
+    buttonFn: (navigation) => {
+      navigation.navigate(NAVIGATION_PAGES.SERIES_RANKING);
+    }
   },
   {
     label: 'Statistics',

+ 16 - 0
src/screens/InAppScreens/TravellersScreen/utils/types.ts

@@ -36,3 +36,19 @@ export interface TriumphsData {
   text: string;
   icon: string;
 }
+
+export interface SeriesRanking {
+  user_id: number;
+  score: number;
+  avatar: string | null;
+  first_name: string;
+  last_name: string;
+  age: number;
+  authenticated: 0 | 1;
+  badge_nm: 0 | 1;
+  badge_un: 0 | 1;
+  flag1: string;
+  flag2: string | null;
+  rip: boolean;
+  recent_score_increase: boolean;
+}

+ 8 - 3
src/types/api.ts

@@ -13,7 +13,8 @@ export enum API_ROUTE {
   TRIPS = 'trips',
   SLOW = 'slow',
   QUICK_ENTER = 'quickEnter',
-  TRIUMPHS = 'triumphs'
+  TRIUMPHS = 'triumphs',
+  SERIES_RANKING = 'series-ranking'
 }
 
 export enum API_ENDPOINT {
@@ -71,7 +72,9 @@ export enum API_ENDPOINT {
   GET_REGIONS_DARE = 'get-regions-dare',
   SET_DARE_REGION = 'updateDARE',
   GET_TRIUMPHS_DATES = 'get-dates-app',
-  GET_TRIUMPHS_DATA = 'get-data-app'
+  GET_TRIUMPHS_DATA = 'get-data-app',
+  GET_SERIES_GROUPS_RANKING = 'get-series-groups-ranking',
+  GET_SERIES_RANKING = 'get-series-ranking'
 }
 
 export enum API {
@@ -128,7 +131,9 @@ export enum API {
   GET_REGIONS_DARE = `${API_ROUTE.REGIONS}/${API_ENDPOINT.GET_REGIONS_DARE}`,
   SET_DARE_REGION = `${API_ROUTE.QUICK_ENTER}/${API_ENDPOINT.SET_DARE_REGION}`,
   GET_TRIUMPHS_DATES = `${API_ROUTE.TRIUMPHS}/${API_ENDPOINT.GET_TRIUMPHS_DATES}`,
-  GET_TRIUMPHS_DATA = `${API_ROUTE.TRIUMPHS}/${API_ENDPOINT.GET_TRIUMPHS_DATA}`
+  GET_TRIUMPHS_DATA = `${API_ROUTE.TRIUMPHS}/${API_ENDPOINT.GET_TRIUMPHS_DATA}`,
+  GET_SERIES_GROUPS_RANKING = `${API_ROUTE.SERIES_RANKING}/${API_ENDPOINT.GET_SERIES_GROUPS_RANKING}`,
+  GET_SERIES_RANKING = `${API_ROUTE.SERIES_RANKING}/${API_ENDPOINT.GET_SERIES_RANKING}`
 }
 
 export type BaseAxiosError = AxiosError;

+ 2 - 0
src/types/navigation.ts

@@ -36,4 +36,6 @@ export enum NAVIGATION_PAGES {
   REGIONS = 'inAppRegions',
   DARE = 'inAppDare',
   TRIUMPHS = 'inAppTriumphs',
+  SERIES_RANKING = 'inAppSeriesRanking',
+  SERIES_RANKING_LIST = 'inAppSeriesRankingList',
 }