Преглед на файлове

pressable profile updates

Viktoriia преди 10 месеца
родител
ревизия
0a25505f6e

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

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

+ 22 - 0
src/modules/api/user/queries/use-post-get-update.tsx

@@ -0,0 +1,22 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { userQueryKeys } from '../user-query-keys';
+import { type PostGetUpdateReturn, userApi } from '../user-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const usePostGetUpdateQuery = <T extends string>(
+  token: string,
+  userId: number,
+  type: T,
+  enabled: boolean
+) => {
+  return useQuery<PostGetUpdateReturn<T>, BaseAxiosError>({
+    queryKey: userQueryKeys.getUpdate(userId, type),
+    queryFn: async () => {
+      const response = await userApi.getUpdate(token, userId, type);
+      return response.data;
+    },
+    enabled
+  });
+};

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

@@ -293,6 +293,57 @@ export interface PostGetMapYearsReturn extends ResponseType {
   };
 }
 
+export interface NewNM {
+  new_nm: {
+    flag1: string;
+    flag2: string | null;
+    id: number;
+    name: string;
+  }[];
+  visited_regions: {
+    flag1: string;
+    flag2: string | null;
+    id: number;
+    name: string;
+  }[];
+}
+
+export interface UnOrUnp {
+  visited_countries: {
+    country: string;
+    flag: string;
+  }[];
+}
+
+export interface Dare {
+  flag1: string;
+  flag2: string | null;
+  id: number;
+  name: string;
+}
+
+export interface SeriesOrWhs {
+  icon: string;
+  id: number;
+  item: string;
+  series: string;
+  app_icon: string;
+}
+
+type PostGetUpdateReturnData<T extends string> = T extends 'nm'
+  ? NewNM
+  : T extends 'un' | 'unp'
+    ? UnOrUnp
+    : T extends 'dare'
+      ? Dare[]
+      : T extends 'series' | 'whs'
+        ? SeriesOrWhs[]
+        : never;
+
+export interface PostGetUpdateReturn<T extends string> extends ResponseType {
+  data: PostGetUpdateReturnData<T>;
+}
+
 export const userApi = {
   getProfileData: (token: string) =>
     request.postForm<PostGetProfileData>(API.GET_USER_SETTINGS_DATA, { token }),
@@ -326,5 +377,7 @@ export const userApi = {
       profile_id
     }),
   getMapYears: (token: string, profile_id: number) =>
-    request.postForm<PostGetMapYearsReturn>(API.GET_MAP_YEARS, { token, profile_id })
+    request.postForm<PostGetMapYearsReturn>(API.GET_MAP_YEARS, { token, profile_id }),
+  getUpdate: <T extends string>(token: string, profile_id: number, type: T) =>
+    request.postForm<PostGetUpdateReturn<T>>(API.GET_UPDATE, { token, profile_id, type })
 };

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

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

+ 41 - 14
src/screens/InAppScreens/ProfileScreen/Components/PersonalInfo.tsx

@@ -1,6 +1,6 @@
 import { FC, useCallback, useEffect, useState } from 'react';
 import { TouchableOpacity, View, Text, Image, Dimensions } from 'react-native';
-import { Series, usePostGetProfileRegions } from '@api/user';
+import { Series, usePostGetProfileRegions, usePostGetUpdateQuery } from '@api/user';
 import { NavigationProp } from '@react-navigation/native';
 import Modal from 'react-native-modal';
 import Tooltip from 'react-native-walkthrough-tooltip';
@@ -22,6 +22,7 @@ import { NAVIGATION_PAGES } from 'src/types';
 import { AvatarWithInitials, WarningModal } from 'src/components';
 import { usePostFriendRequestMutation, usePostUpdateFriendStatusMutation } from '@api/friends';
 import FriendStatus from './FriendStatus';
+import UpdatesRenderer from '../UpdatesRenderer';
 
 type PersonalInfoProps = {
   data: {
@@ -88,8 +89,11 @@ export const PersonalInfo: FC<PersonalInfoProps> = ({
     action: () => {},
     title: ''
   });
+  const [isUpdatesModalVisible, setIsUpdatesModalVisible] = useState(false);
+  const [updateType, setUpdateType] = useState<string>('un');
 
   const { data: regions } = usePostGetProfileRegions(token as string, userId, type);
+  const { data: update } = usePostGetUpdateQuery(token as string, userId, updateType, true);
 
   useEffect(() => {
     if (data.isFriend === 1) {
@@ -188,6 +192,11 @@ export const PersonalInfo: FC<PersonalInfoProps> = ({
     [updateFriendStatus, token, data.friendDbId]
   );
 
+  const handleOpenUpdates = (type: string) => {
+    setUpdateType(type);
+    setIsUpdatesModalVisible(true);
+  };
+
   const screenWidth = Dimensions.get('window').width;
   const availableWidth = screenWidth * (1 - 2 * SCREEN_PADDING_PERCENT);
   const maxAvatars = Math.floor(availableWidth / (AVATAR_SIZE - AVATAR_MARGIN)) - 2;
@@ -321,7 +330,7 @@ export const PersonalInfo: FC<PersonalInfoProps> = ({
           <InfoItem title={'Visited in the last 90 days'}>
             <View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
               {updates.un_visited && updates.un_visited > 0 ? (
-                <View style={styles.updates}>
+                <TouchableOpacity style={styles.updates} onPress={() => handleOpenUpdates('un')}>
                   <FlagsIcon fill={Colors.DARK_BLUE} height={20} width={20} />
                   {updates.un_new && updates.un_new > 0 ? (
                     <View>
@@ -333,11 +342,11 @@ export const PersonalInfo: FC<PersonalInfoProps> = ({
                   ) : (
                     <Text style={styles.updatesText}>{updates.un_visited} UN countries</Text>
                   )}
-                </View>
+                </TouchableOpacity>
               ) : null}
 
               {updates.unp_visited && updates.unp_visited > 0 ? (
-                <View style={styles.updates}>
+                <TouchableOpacity style={styles.updates} onPress={() => handleOpenUpdates('unp')}>
                   <UNPIcon fill={Colors.DARK_BLUE} height={20} width={20} />
                   {updates.unp_new && updates.unp_new > 0 ? (
                     <View>
@@ -349,11 +358,11 @@ export const PersonalInfo: FC<PersonalInfoProps> = ({
                   ) : (
                     <Text style={styles.updatesText}>{updates.unp_visited} UN+ states</Text>
                   )}
-                </View>
+                </TouchableOpacity>
               ) : null}
 
               {updates.nm_visited && updates.nm_visited > 0 ? (
-                <View style={styles.updates}>
+                <TouchableOpacity style={styles.updates} onPress={() => handleOpenUpdates('nm')}>
                   <RegionsIcon fill={Colors.DARK_BLUE} height={20} width={20} />
                   {updates.nm_new && updates.nm_new > 0 ? (
                     <View>
@@ -365,28 +374,31 @@ export const PersonalInfo: FC<PersonalInfoProps> = ({
                   ) : (
                     <Text style={styles.updatesText}>{updates.nm_visited} NM regions</Text>
                   )}
-                </View>
+                </TouchableOpacity>
               ) : null}
 
               {updates.new_dare && updates.new_dare > 0 ? (
-                <View style={styles.updates}>
+                <TouchableOpacity style={styles.updates} onPress={() => handleOpenUpdates('dare')}>
                   <CompassIcon fill={Colors.DARK_BLUE} height={20} width={20} />
-                  <Text style={styles.updatesText}>{updates.new_dare} new DARE</Text>
-                </View>
+                  <Text style={styles.updatesText}>{updates.new_dare} new DAREs</Text>
+                </TouchableOpacity>
               ) : null}
 
               {updates.new_series && updates.new_series > 0 ? (
-                <View style={styles.updates}>
+                <TouchableOpacity
+                  style={styles.updates}
+                  onPress={() => handleOpenUpdates('series')}
+                >
                   <SeriesIcon fill={Colors.DARK_BLUE} height={20} width={20} />
                   <Text style={styles.updatesText}>{updates.new_series} new Series</Text>
-                </View>
+                </TouchableOpacity>
               ) : null}
 
               {updates.new_whs && updates.new_whs > 0 ? (
-                <View style={styles.updates}>
+                <TouchableOpacity style={styles.updates} onPress={() => handleOpenUpdates('whs')}>
                   <WHSIcon fill={Colors.DARK_BLUE} height={20} width={20} />
                   <Text style={styles.updatesText}>{updates.new_whs} new WHS</Text>
-                </View>
+                </TouchableOpacity>
               ) : null}
             </View>
           </InfoItem>
@@ -455,6 +467,21 @@ export const PersonalInfo: FC<PersonalInfoProps> = ({
         <RegionsRenderer type={type} regions={regions} setIsModalVisible={setIsModalVisible} />
       </Modal>
 
+      <Modal
+        isVisible={isUpdatesModalVisible}
+        onBackdropPress={() => setIsUpdatesModalVisible(false)}
+        onBackButtonPress={() => setIsUpdatesModalVisible(false)}
+        style={styles.modal}
+        statusBarTranslucent={true}
+        presentationStyle="overFullScreen"
+      >
+        <UpdatesRenderer
+          type={updateType}
+          updates={update ?? null}
+          setIsModalVisible={setIsUpdatesModalVisible}
+        />
+      </Modal>
+
       <WarningModal
         type={modalInfo.type}
         isVisible={modalInfo.isVisible}

+ 200 - 0
src/screens/InAppScreens/ProfileScreen/UpdatesRenderer/index.tsx

@@ -0,0 +1,200 @@
+import React from 'react';
+import { View, Text, Image, TouchableOpacity } from 'react-native';
+import { styles } from './styles';
+import { Loading } from 'src/components';
+
+import CloseSVG from 'assets/icons/close.svg';
+import { API_HOST } from 'src/constants';
+import { FlashList } from '@shopify/flash-list';
+import { useNavigation } from '@react-navigation/native';
+import { Dare, NewNM, PostGetUpdateReturn, SeriesOrWhs, UnOrUnp } from '@api/user';
+import { NAVIGATION_PAGES } from 'src/types';
+
+interface UpdatesRendererProps<T extends string> {
+  type: T;
+  updates: PostGetUpdateReturn<T> | null;
+  setIsModalVisible: (value: boolean) => void;
+}
+
+const UpdatesRenderer = <T extends string>({
+  type,
+  updates,
+  setIsModalVisible
+}: UpdatesRendererProps<T>) => {
+  const navigation = useNavigation();
+  const flashlistConfig = {
+    waitForInteraction: true,
+    itemVisiblePercentThreshold: 50,
+    minimumViewTime: 1000
+  };
+
+  const handlePress = (item: any) => {
+    setIsModalVisible(false);
+    navigation.navigate(
+      ...([
+        NAVIGATION_PAGES.REGION_PREVIEW,
+        {
+          regionId: item.id,
+          type: type === 'nm' ? 'nm' : 'dare',
+          disabled: false,
+          isProfileScreen: true
+        }
+      ] as never)
+    );
+  };
+
+  const renderContent = () => {
+    if (!updates) return null;
+
+    const renderHeader = (headerText: string) => (
+      <RegionsModalHeader textHeader={headerText} onRequestClose={() => setIsModalVisible(false)} />
+    );
+
+    switch (type) {
+      case 'nm':
+        return (
+          <>
+            {renderHeader('Regions visited')}
+            <FlashList
+              viewabilityConfig={flashlistConfig}
+              estimatedItemSize={50}
+              data={(updates.data as NewNM).visited_regions}
+              renderItem={({ item }) => (
+                <TouchableOpacity onPress={() => handlePress(item)} style={styles.item}>
+                  <Image
+                    source={{
+                      uri: API_HOST + '/img/flags_new/' + item.flag1
+                    }}
+                    style={styles.regionsFlag}
+                  />
+                  {item.flag2 ? (
+                    <Image
+                      source={{
+                        uri: API_HOST + '/img/flags_new/' + item.flag2
+                      }}
+                      style={[styles.regionsFlag, { marginLeft: -18 }]}
+                    />
+                  ) : null}
+                  <Text style={styles.regionName}>{item.name}</Text>
+                </TouchableOpacity>
+              )}
+              keyExtractor={(item) => item.id.toString()}
+              showsVerticalScrollIndicator={false}
+              nestedScrollEnabled={true}
+              contentContainerStyle={{ paddingTop: 8 }}
+            />
+          </>
+        );
+      case 'un':
+      case 'unp':
+        return (
+          <>
+            {renderHeader('Countries visited')}
+            <FlashList
+              viewabilityConfig={flashlistConfig}
+              estimatedItemSize={50}
+              data={(updates.data as UnOrUnp).visited_countries}
+              renderItem={({ item }) => (
+                <View style={styles.item}>
+                  <Image
+                    source={{
+                      uri: API_HOST + '/img/flags_new/' + item.flag
+                    }}
+                    style={styles.regionsFlag}
+                  />
+                  <Text style={styles.regionName}>{item.country}</Text>
+                </View>
+              )}
+              keyExtractor={(item) => item.country.toString()}
+              showsVerticalScrollIndicator={false}
+              contentContainerStyle={{ paddingTop: 8 }}
+            />
+          </>
+        );
+      case 'dare':
+        return (
+          <>
+            {renderHeader('New DAREs visited')}
+            <FlashList
+              viewabilityConfig={flashlistConfig}
+              estimatedItemSize={50}
+              data={updates.data as Dare[]}
+              renderItem={({ item }) => (
+                <TouchableOpacity style={styles.item} onPress={() => handlePress(item)}>
+                  <Image
+                    source={{
+                      uri: API_HOST + '/img/flags_new/' + item.flag1
+                    }}
+                    style={styles.regionsFlag}
+                  />
+                  {item.flag2 ? (
+                    <Image
+                      source={{
+                        uri: API_HOST + '/img/flags_new/' + item.flag2
+                      }}
+                      style={[styles.regionsFlag, { marginLeft: -18 }]}
+                    />
+                  ) : null}
+                  <Text style={styles.regionName}>{item.name}</Text>
+                </TouchableOpacity>
+              )}
+              keyExtractor={(item) => item.id.toString()}
+              showsVerticalScrollIndicator={false}
+              contentContainerStyle={{ paddingTop: 8 }}
+            />
+          </>
+        );
+      case 'whs':
+      case 'series':
+        return (
+          <>
+            {renderHeader(type === 'whs' ? 'New WHS visited' : 'New Series visited')}
+            <FlashList
+              viewabilityConfig={flashlistConfig}
+              estimatedItemSize={50}
+              data={updates.data as SeriesOrWhs[]}
+              renderItem={({ item }) => (
+                <View style={styles.item}>
+                  <Image
+                    source={{
+                      uri: API_HOST + item.app_icon
+                    }}
+                    style={{ width: 32, height: 32 }}
+                  />
+                  <Text style={[styles.regionName, { fontFamily: 'redhat-700' }]}>
+                    {item.series}
+                  </Text>
+                  <Text style={[styles.regionName, { flex: 2 }]}>{item.item}</Text>
+                </View>
+              )}
+              keyExtractor={(item, index) => item.item.toString() + index.toString()}
+              showsVerticalScrollIndicator={false}
+              contentContainerStyle={{ paddingTop: 8 }}
+            />
+          </>
+        );
+    }
+  };
+
+  return <View style={styles.modalContent}>{updates?.data ? renderContent() : <Loading />}</View>;
+};
+
+const RegionsModalHeader = ({
+  textHeader,
+  onRequestClose
+}: {
+  textHeader: string;
+  onRequestClose: () => void;
+}) => {
+  return (
+    <View style={styles.header}>
+      <TouchableOpacity onPress={onRequestClose} style={{ padding: 6 }}>
+        <CloseSVG />
+      </TouchableOpacity>
+      <Text style={styles.headerText}>{textHeader}</Text>
+      <View style={{ height: 30, width: 30 }} />
+    </View>
+  );
+};
+
+export default UpdatesRenderer;

+ 46 - 0
src/screens/InAppScreens/ProfileScreen/UpdatesRenderer/styles.tsx

@@ -0,0 +1,46 @@
+import { StyleSheet } from 'react-native';
+import { Colors } from '../../../../theme';
+import { getFontSize } from '../../../../utils';
+
+export const styles = StyleSheet.create({
+  modalContent: {
+    backgroundColor: 'white',
+    borderRadius: 15,
+    paddingHorizontal: 16,
+    gap: 12,
+    paddingVertical: 12,
+    height: '90%'
+  },
+  regionName: {
+    fontSize: 12,
+    fontWeight: '600',
+    color: Colors.DARK_BLUE,
+    flex: 1
+  },
+  regionsFlag: {
+    width: 32,
+    height: 32,
+    borderRadius: 32 / 2,
+    borderWidth: 1,
+    borderColor: Colors.FILL_LIGHT
+  },
+  header: {
+    width: '100%',
+    display: 'flex',
+    justifyContent: 'space-between',
+    flexDirection: 'row',
+    alignItems: 'center'
+  },
+  headerText: {
+    fontFamily: 'redhat-600',
+    fontSize: getFontSize(14),
+    color: Colors.DARK_BLUE
+  },
+  item: {
+    flexDirection: 'row',
+    gap: 8,
+    alignItems: 'center',
+    flex: 1,
+    marginBottom: 12
+  }
+});

+ 4 - 2
src/types/api.ts

@@ -119,7 +119,8 @@ export enum API_ENDPOINT {
   GET_FIXERS = 'get-for-country',
   SAVE_RATING = 'save-rating-app',
   ADD_FIXER = 'add-fixer',
-  EDIT_FIXER = 'edit-fixer'
+  EDIT_FIXER = 'edit-fixer',
+  GET_UPDATE = 'get-update'
 }
 
 export enum API {
@@ -217,7 +218,8 @@ export enum API {
   GET_FIXERS = `${API_ROUTE.FIXERS}/${API_ENDPOINT.GET_FIXERS}`,
   SAVE_RATING = `${API_ROUTE.FIXERS}/${API_ENDPOINT.SAVE_RATING}`,
   ADD_FIXER = `${API_ROUTE.FIXERS}/${API_ENDPOINT.ADD_FIXER}`,
-  EDIT_FIXER = `${API_ROUTE.FIXERS}/${API_ENDPOINT.EDIT_FIXER}`
+  EDIT_FIXER = `${API_ROUTE.FIXERS}/${API_ENDPOINT.EDIT_FIXER}`,
+  GET_UPDATE = `${API_ROUTE.PROFILE}/${API_ENDPOINT.GET_UPDATE}`
 }
 
 export type BaseAxiosError = AxiosError;