Просмотр исходного кода

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

Viktoriia 1 год назад
Родитель
Сommit
080788bc4f

+ 6 - 1
Route.tsx

@@ -26,6 +26,7 @@ import LPIRanking from './src/screens/InAppScreens/TravellersScreen/LPIRankingSc
 import InMemoriamScreen from './src/screens/InAppScreens/TravellersScreen/InMemoriamScreen';
 import InHistoryScreen from './src/screens/InAppScreens/TravellersScreen/InHistoryScreen';
 import UNMastersScreen from './src/screens/InAppScreens/TravellersScreen/UNMasters';
+import SeriesScreen from 'src/screens/InAppScreens/TravelsScreen/Series';
 
 import { NAVIGATION_PAGES } from './src/types';
 import { storage, StoreType } from './src/storage';
@@ -146,9 +147,13 @@ const Route = () => {
               {() => (
                 <ScreenStack.Navigator screenOptions={screenOptions}>
                   <ScreenStack.Screen
-                    name={NAVIGATION_PAGES.TRAVELLERS_TAB}
+                    name={NAVIGATION_PAGES.TRAVELS_TAB}
                     component={TravelsScreen}
                   />
+                  <ScreenStack.Screen
+                    name={NAVIGATION_PAGES.SERIES}
+                    component={SeriesScreen}
+                  />
                 </ScreenStack.Navigator>
               )}
             </BottomTab.Screen>

+ 41 - 0
package-lock.json

@@ -37,15 +37,18 @@
         "react": "18.2.0",
         "react-native": "0.72.6",
         "react-native-calendar-picker": "^7.1.4",
+        "react-native-device-detection": "^0.2.1",
         "react-native-gesture-handler": "~2.12.0",
         "react-native-keyboard-aware-scroll-view": "^0.9.5",
         "react-native-maps": "1.7.1",
         "react-native-mmkv": "^2.11.0",
+        "react-native-modal": "^13.0.1",
         "react-native-pager-view": "6.2.0",
         "react-native-reanimated": "~3.3.0",
         "react-native-render-html": "^6.3.4",
         "react-native-safe-area-context": "4.6.3",
         "react-native-screens": "~3.22.0",
+        "react-native-searchable-dropdown-kj": "^1.9.1",
         "react-native-svg": "13.9.0",
         "react-native-tab-view": "^3.5.2",
         "yup": "^1.3.3",
@@ -15935,6 +15938,14 @@
         "react": "18.2.0"
       }
     },
+    "node_modules/react-native-animatable": {
+      "version": "1.3.3",
+      "resolved": "https://registry.npmjs.org/react-native-animatable/-/react-native-animatable-1.3.3.tgz",
+      "integrity": "sha512-2ckIxZQAsvWn25Ho+DK3d1mXIgj7tITkrS4pYDvx96WyOttSvzzFeQnM2od0+FUMzILbdHDsDEqZvnz1DYNQ1w==",
+      "dependencies": {
+        "prop-types": "^15.7.2"
+      }
+    },
     "node_modules/react-native-calendar-picker": {
       "version": "7.1.4",
       "resolved": "https://registry.npmjs.org/react-native-calendar-picker/-/react-native-calendar-picker-7.1.4.tgz",
@@ -15954,6 +15965,11 @@
         "react-native": "*"
       }
     },
+    "node_modules/react-native-device-detection": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/react-native-device-detection/-/react-native-device-detection-0.2.1.tgz",
+      "integrity": "sha512-0ErWdxoB4DPLuzzjgS3nUyzauo1YoJ0QjQJZMjx0dr3MGdF3SE9T3Lb2lKj3kPsTQYy5qnlWZ24DmQkHZlIjgw=="
+    },
     "node_modules/react-native-gesture-handler": {
       "version": "2.12.1",
       "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.12.1.tgz",
@@ -16017,6 +16033,19 @@
         "react-native": ">=0.71.0"
       }
     },
+    "node_modules/react-native-modal": {
+      "version": "13.0.1",
+      "resolved": "https://registry.npmjs.org/react-native-modal/-/react-native-modal-13.0.1.tgz",
+      "integrity": "sha512-UB+mjmUtf+miaG/sDhOikRfBOv0gJdBU2ZE1HtFWp6UixW9jCk/bhGdHUgmZljbPpp0RaO/6YiMmQSSK3kkMaw==",
+      "dependencies": {
+        "prop-types": "^15.6.2",
+        "react-native-animatable": "1.3.3"
+      },
+      "peerDependencies": {
+        "react": "*",
+        "react-native": ">=0.65.0"
+      }
+    },
     "node_modules/react-native-pager-view": {
       "version": "6.2.0",
       "resolved": "https://registry.npmjs.org/react-native-pager-view/-/react-native-pager-view-6.2.0.tgz",
@@ -16089,6 +16118,18 @@
         "react-native": "*"
       }
     },
+    "node_modules/react-native-searchable-dropdown-kj": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/react-native-searchable-dropdown-kj/-/react-native-searchable-dropdown-kj-1.9.1.tgz",
+      "integrity": "sha512-+I+QBjQik/EAvgsvF7MVUvriSpjprT7/VDd+ZvdHA/quY6zujgvTCjeQ7xUfzTuneokYyA8TgWSO0NkcMj35ZQ==",
+      "dependencies": {
+        "lodash": "*"
+      },
+      "peerDependencies": {
+        "react": "*",
+        "react-native": "*"
+      }
+    },
     "node_modules/react-native-svg": {
       "version": "13.9.0",
       "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-13.9.0.tgz",

+ 51 - 0
src/components/HorizontalTabView/index.tsx

@@ -0,0 +1,51 @@
+import React from 'react';
+import { View, Text } from 'react-native';
+import { TabView, TabBar, Route } from 'react-native-tab-view';
+
+import { styles } from './styles';
+import { Colors } from 'src/theme';
+
+export const HorizontalTabView = ({
+  index,
+  setIndex,
+  routes,
+  renderScene
+}: {
+  index: number;
+  setIndex: React.Dispatch<React.SetStateAction<number>>;
+  routes: Route[];
+  renderScene: (props: any) => React.ReactNode;
+}) => {
+
+  const renderTabBar = (props: any) => (
+    <TabBar
+      {...props}
+      renderLabel={({ route, focused }) => (
+        <View style={[styles.tabLabelContainer, focused ? styles.tabLabelFocused : null]}>
+          <Text style={[styles.label, focused ? styles.labelFocused : null]}>
+            {route.title}
+          </Text>
+        </View>
+      )}
+      scrollEnabled={true}
+      indicatorStyle={styles.indicator}
+      style={styles.tabBar}
+      activeColor={Colors.ORANGE}
+      inactiveColor={Colors.DARK_BLUE}
+      tabStyle={styles.tabStyle}
+      pressColor={'transparent'}
+    />
+  );
+
+  return (
+    <TabView
+      navigationState={{ index, routes }}
+      renderScene={renderScene}
+      onIndexChange={setIndex}
+      animationEnabled={true}
+      swipeEnabled={true}
+      style={styles.tabView}
+      renderTabBar={renderTabBar}
+    />
+  );
+};

+ 44 - 0
src/components/HorizontalTabView/styles.tsx

@@ -0,0 +1,44 @@
+import { StyleSheet } from 'react-native';
+import { Colors } from 'src/theme';
+
+export const styles = StyleSheet.create({
+  tabBar: {
+    backgroundColor: 'transparent',
+    elevation: 0,
+    shadowOpacity: 0,
+    marginTop: 0
+  },
+  tabView: {
+    marginTop: -5
+  },
+  tabStyle: {
+    width: 'auto',
+    padding: 0,
+    marginHorizontal: 2,
+    backgroundColor: 'transparent'
+  },
+  tabLabelContainer: {
+    borderRadius: 20,
+    paddingHorizontal: 12,
+    paddingVertical: 10,
+    backgroundColor: 'transparent',
+    borderColor: 'rgba(15, 63, 79, 0.3)',
+    borderWidth: 1
+  },
+  tabLabelFocused: {
+    backgroundColor: Colors.ORANGE,
+    borderColor: Colors.ORANGE
+  },
+  label: {
+    fontSize: 12,
+    textTransform: 'none',
+    color: Colors.DARK_BLUE,
+    fontWeight: '600'
+  },
+  labelFocused: {
+    color: 'white'
+  },
+  indicator: {
+    height: 0
+  }
+});

+ 1 - 0
src/components/index.ts

@@ -14,3 +14,4 @@ export * from './MenuButton';
 export * from './AvatarWithInitials';
 export * from './Loading';
 export * from './WarningModal';
+export * from './HorizontalTabView';

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

@@ -1 +1,3 @@
 export * from './use-post-get-series';
+export * from './use-post-get-series-groups';
+export * from './use-post-get-series-with-group'

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

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

+ 22 - 0
src/modules/api/series/queries/use-post-get-series-with-group.tsx

@@ -0,0 +1,22 @@
+import { useQuery } from '@tanstack/react-query';
+import { useMutation } from '@tanstack/react-query';
+
+import { seriesQueryKeys } from '../series-query-keys';
+import { seriesApi, type PostGetSeriesList } from '../series-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const useGetSeriesWithGroup = () => {
+  return useMutation<
+  PostGetSeriesList,
+    BaseAxiosError,
+    { token: string; group: string },
+    PostGetSeriesList
+  >({
+    mutationKey: seriesQueryKeys.getSeriesWithGroup(),
+    mutationFn: async (variables) => {
+      const response = await seriesApi.getSeriesWithGroup(variables.token, variables.group);
+      return response.data;
+    }
+  });
+};

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

@@ -15,7 +15,31 @@ export interface PostGetSeries extends ResponseType {
   }[];
 }
 
+export interface PostGetSeriesGroups extends ResponseType {
+  data: {
+    id: number;
+    name: string;
+  }[];
+}
+
+export interface PostGetSeriesList extends ResponseType {
+  data: {
+    id: number;
+    name: string;
+    icon: string;
+    count: number;
+    new: number;
+    score: number;
+    icon_png: string;
+    count2: number;
+  }[]
+}
+
 export const seriesApi = {
   getSeries: (token: string | null, regions: string) =>
-    request.postForm<PostGetSeries>(API.SERIES, { token, regions })
+    request.postForm<PostGetSeries>(API.SERIES, { token, regions }),
+  getSeriesGroups: () =>
+    request.postForm<PostGetSeriesGroups>(API.SERIES_GROUPS),
+  getSeriesWithGroup: (token: string, group: string) =>
+    request.postForm<PostGetSeriesList>(API.SERIES_WITH_GROUP, { token, group }),
 };

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

@@ -1,3 +1,5 @@
 export const seriesQueryKeys = {
   fetchSeriesData: () => ['fetchSeriesData'] as const,
+  getSeriesGroups: () => ['getSeriesGroups'] as const,
+  getSeriesWithGroup: () => ['getSeriesWithGroup'] as const,
 };

+ 146 - 0
src/screens/InAppScreens/TravelsScreen/Series/index.tsx

@@ -0,0 +1,146 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import { View, Text, FlatList, Image, TouchableOpacity } from 'react-native';
+
+import { Header, PageWrapper, HorizontalTabView, Loading } from 'src/components';
+import { useGetSeriesGroups, useGetSeriesWithGroup } from '@api/series';
+import { useFocusEffect } from '@react-navigation/native';
+
+import { API_HOST } from 'src/constants';
+import { StoreType, storage } from 'src/storage';
+import { styles } from './styles';
+
+interface SeriesGroup {
+  key: string;
+  title: string;
+}
+
+interface SeriesList {
+  id: number;
+  name: string;
+  icon: string;
+  count: number;
+  new: number;
+  score: number;
+  icon_png: string;
+  count2: number;
+}
+
+const SeriesScreen = () => {
+  const [isLoading, setIsLoading] = useState(true);
+  const [index, setIndex] = useState(0);
+  const [routes, setRoutes] = useState<SeriesGroup[]>([]);
+  const { data } = useGetSeriesGroups(true);
+
+  useFocusEffect(
+    useCallback(() => {
+      const fetchRanking = async () => {
+        let staticGroups = [
+          {
+            key: '-1',
+            title: 'Top of the tops'
+          },
+          {
+            key: '-2',
+            title: 'Milestones'
+          },
+          {
+            key: 'all',
+            title: 'All series'
+          }
+        ];
+        const routesData = data?.data?.map((item) => ({
+          key: item.id.toString(),
+          title: item.name
+        }));
+
+        routesData && staticGroups.push(...routesData);
+
+        setRoutes(staticGroups);
+        setIsLoading(false);
+      };
+
+      if (data && data.data) {
+        fetchRanking();
+      }
+    }, [data])
+  );
+
+  if (isLoading) return <Loading />;
+
+  return (
+    <PageWrapper>
+      <Header label={'Series'} />
+      <HorizontalTabView
+        index={index}
+        setIndex={setIndex}
+        routes={routes}
+        renderScene={({ route }: { route: SeriesGroup }) => <SeriesList groupId={route.key} />}
+      />
+    </PageWrapper>
+  );
+};
+
+const SeriesList = React.memo(({ groupId }: { groupId: string }) => {
+  const [seriesData, setSeriesData] = useState<SeriesList[]>([]);
+  const [isLoading, setIsLoading] = useState(true);
+
+  const token = storage.get('token', StoreType.STRING) as string;
+  const { mutateAsync } = useGetSeriesWithGroup();
+
+  useEffect(() => {
+    const fetchSeriesData = async () => {
+      try {
+        await mutateAsync(
+          { group: groupId, token: token },
+          {
+            onSuccess: (data) => {
+              setSeriesData(data.data);
+            }
+          }
+        );
+      } catch (error) {
+        console.error('Failed to fetch series data', error);
+      } finally {
+        setIsLoading(false);
+      }
+    };
+
+    fetchSeriesData();
+  }, [groupId]);
+
+  if (isLoading) return <Loading />;
+
+  const renderItem = ({ item }: { item: SeriesList }) => {
+    return (
+      <TouchableOpacity style={styles.itemContainer}>
+        <Image style={styles.icon} source={{ uri: API_HOST + item.icon_png }} />
+
+        <View style={styles.infoContainer}>
+          <View style={styles.titleContainer}>
+            <Text style={styles.title}>{item.name}</Text>
+            {item.new === 1 && <Text style={styles.textNew}>NEW</Text>}
+          </View>
+
+          <View style={styles.detailsContainer}>
+            <Text style={styles.details}>Total objects: {item.count}</Text>
+            <Text style={styles.count}>
+              {item.score} / {item.count}
+            </Text>
+          </View>
+        </View>
+      </TouchableOpacity>
+    );
+  };
+
+  return (
+    <FlatList
+      horizontal={false}
+      data={seriesData}
+      renderItem={renderItem}
+      keyExtractor={(item) => item.id.toString()}
+      showsVerticalScrollIndicator={false}
+    />
+  );
+});
+
+export default SeriesScreen;

+ 55 - 0
src/screens/InAppScreens/TravelsScreen/Series/styles.tsx

@@ -0,0 +1,55 @@
+import { StyleSheet } from 'react-native';
+import { Colors } from '../../../../theme';
+
+export const styles = StyleSheet.create({
+  scene: {
+    flex: 1,
+    alignItems: 'center',
+    justifyContent: 'center'
+  },
+  itemContainer: {
+    flexDirection: 'row',
+    padding: 10,
+    alignItems: 'center',
+  },
+  icon: {
+    width: 48,
+    height: 48,
+    marginRight: 10,
+    resizeMode: 'contain',
+  },
+  infoContainer: {
+    flex: 1,
+    justifyContent: 'center',
+  },
+  titleContainer: {
+    display: 'flex',
+    flexDirection: 'row',
+    gap: 2
+  },
+  title: {
+    fontSize: 14,
+    fontWeight: 'bold',
+    color: Colors.DARK_BLUE,
+  },
+  textNew: {
+    color: Colors.ORANGE,
+    fontSize: 10,
+    fontWeight: '800'
+  },
+  detailsContainer: {
+    display: 'flex',
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center'
+  },
+  details: {
+    fontSize: 12,
+    color: '#808080',
+  },
+  count: {
+    color: Colors.DARK_BLUE,
+    fontSize: 14,
+    fontWeight: '700'
+  }
+});

+ 50 - 33
src/screens/InAppScreens/TravelsScreen/index.tsx

@@ -1,8 +1,7 @@
-import React from 'react';
+import React, { useState } from 'react';
 
 import { Colors } from '../../../theme';
-import { PageWrapper, MenuButton } from '../../../components';
-
+import { PageWrapper, MenuButton, WarningModal } from '../../../components';
 import { MenuButtonType } from '../../../types/components';
 
 import FlagsIcon from '../../../../assets/icons/travels-section/flags.svg';
@@ -13,38 +12,51 @@ import EarthIcon from '../../../../assets/icons/travels-section/earth.svg';
 import TripIcon from '../../../../assets/icons/travels-section/trip.svg';
 import ImagesIcon from '../../../../assets/icons/travels-section/images.svg';
 
-const buttons: MenuButtonType[] = [
-  {
-    label: 'Countries',
-    icon: <FlagsIcon fill={Colors.DARK_BLUE} width={20} height={20} />,
-  },
-  {
-    label: 'Regions',
-    icon: <RegionsIcon fill={Colors.DARK_BLUE} width={20} height={20} />,
-  },
-  {
-    label: 'DARE',
-    icon: <MapLocationIcon fill={Colors.DARK_BLUE} width={20} height={20} />,
-  },
-  {
-    label: 'Series',
-    icon: <SeriesIcon fill={Colors.DARK_BLUE} width={20} height={20} />
-  },
-  {
-    label: 'Earth',
-    icon: <EarthIcon fill={Colors.DARK_BLUE} width={20} height={20} />
-  },
-  {
-    label: 'Trips',
-    icon: <TripIcon fill={Colors.DARK_BLUE} width={20} height={20} />,
-  },
-  {
-    label: 'Photos',
-    icon: <ImagesIcon fill={Colors.DARK_BLUE} width={20} height={20} />,
-  },
-];
+import { NAVIGATION_PAGES } from 'src/types';
+import { storage, StoreType } from '../../../storage';
 
 const TravelsScreen = () => {
+  const [isModalVisible, setIsModalVisible] = useState(false);
+  const token = storage.get('token', StoreType.STRING);
+
+  const buttons: MenuButtonType[] = [
+    {
+      label: 'Countries',
+      icon: <FlagsIcon fill={Colors.DARK_BLUE} width={20} height={20} />,
+    },
+    {
+      label: 'Regions',
+      icon: <RegionsIcon fill={Colors.DARK_BLUE} width={20} height={20} />,
+    },
+    {
+      label: 'DARE',
+      icon: <MapLocationIcon fill={Colors.DARK_BLUE} width={20} height={20} />,
+    },
+    {
+      label: 'Series',
+      icon: <SeriesIcon fill={Colors.DARK_BLUE} width={20} height={20} />,
+      buttonFn: (navigation) => {
+        if (!token) {
+          setIsModalVisible(true);
+        } else {
+          navigation.navigate(NAVIGATION_PAGES.SERIES);
+        }
+      }
+    },
+    {
+      label: 'Earth',
+      icon: <EarthIcon fill={Colors.DARK_BLUE} width={20} height={20} />
+    },
+    {
+      label: 'Trips',
+      icon: <TripIcon fill={Colors.DARK_BLUE} width={20} height={20} />,
+    },
+    {
+      label: 'Photos',
+      icon: <ImagesIcon fill={Colors.DARK_BLUE} width={20} height={20} />,
+    },
+  ];
+  
   return (
     <PageWrapper>
       {buttons.map((button, index) => (
@@ -55,6 +67,11 @@ const TravelsScreen = () => {
           key={'travels-button-' + index}
         />
       ))}
+      <WarningModal
+        type={'unauthorized'}
+        isVisible={isModalVisible}
+        onClose={() => setIsModalVisible(false)}
+      />
     </PageWrapper>
   );
 };

+ 4 - 0
src/types/api.ts

@@ -32,6 +32,8 @@ export enum API_ENDPOINT {
   GET_UPDATED_AVATARS = 'get-updates',
   GET_LIST = 'get-list',
   GET_STATISTIC = 'get-stat',
+  SERIES_GROUPS = 'get-series-groups',
+  SERIES_WITH_GROUP = 'get-series-with-group-app',
   GET_COUNTRIES_RANKING = 'get-countries-ranking',
   GET_COUNTRIES_RANKING_LPI = 'get-countries-ranking-lpi',
   GET_COUNTRIES_RANKING_MEMORIAM = 'get-countries-ranking-in-memoriam'
@@ -58,6 +60,8 @@ export enum API {
   GET_UPDATED_AVATARS = `${API_ROUTE.AVATARS}/${API_ENDPOINT.GET_UPDATED_AVATARS}`,
   GET_LIST = `${API_ROUTE.STATISTICS}/${API_ENDPOINT.GET_LIST}`,
   GET_STATISTIC = `${API_ROUTE.STATISTICS}/${API_ENDPOINT.GET_STATISTIC}`,
+  SERIES_GROUPS = `${API_ROUTE.SERIES}/${API_ENDPOINT.SERIES_GROUPS}`,
+  SERIES_WITH_GROUP = `${API_ROUTE.SERIES}/${API_ENDPOINT.SERIES_WITH_GROUP}`,
   GET_COUNTRIES_RANKING = `${API_ROUTE.RANKING}/${API_ENDPOINT.GET_COUNTRIES_RANKING}`,
   GET_COUNTRIES_RANKING_LPI = `${API_ROUTE.RANKING}/${API_ENDPOINT.GET_COUNTRIES_RANKING_LPI}`,
   GET_COUNTRIES_RANKING_MEMORIAM = `${API_ROUTE.RANKING}/${API_ENDPOINT.GET_COUNTRIES_RANKING_MEMORIAM}`

+ 3 - 1
src/types/navigation.ts

@@ -19,5 +19,7 @@ export enum NAVIGATION_PAGES {
   PUBLIC_PROFILE_VIEW = 'publicProfileView',
   EDIT_PERSONAL_INFO = 'editPersonalInfo',
   SETTINGS = 'settings',
-  IN_APP_TRAVELS_TAB = 'Travels'
+  IN_APP_TRAVELS_TAB = 'Travels',
+  TRAVELS_TAB = 'inAppTravels',
+  SERIES = 'inAppSeries'
 }