Explorar o código

countries functionality

Viktoriia hai 11 meses
pai
achega
505329a6d7

+ 11 - 0
Route.tsx

@@ -75,6 +75,7 @@ import { enableScreens } from 'react-native-screens';
 import UsersListScreen from 'src/screens/InAppScreens/MapScreen/UsersListScreen';
 import SuggestSeriesScreen from 'src/screens/InAppScreens/TravelsScreen/SuggestSeriesScreen';
 import MyFriendsScreen from 'src/screens/InAppScreens/ProfileScreen/MyFriendsScreen';
+import CountryViewScreen from 'src/screens/InAppScreens/MapScreen/CountryViewScreen';
 
 enableScreens();
 
@@ -245,6 +246,11 @@ const Route = () => {
                     component={UsersListScreen}
                     options={regionViewScreenOptions}
                   />
+                  <ScreenStack.Screen
+                    name={NAVIGATION_PAGES.COUNTRY_PREVIEW}
+                    component={CountryViewScreen}
+                    options={regionViewScreenOptions}
+                  />
                 </ScreenStack.Navigator>
               )}
             </BottomTab.Screen>
@@ -374,6 +380,11 @@ const Route = () => {
                     component={UsersListScreen}
                     options={regionViewScreenOptions}
                   />
+                  <ScreenStack.Screen
+                    name={NAVIGATION_PAGES.COUNTRY_PREVIEW}
+                    component={CountryViewScreen}
+                    options={regionViewScreenOptions}
+                  />
                 </ScreenStack.Navigator>
               )}
             </BottomTab.Screen>

+ 71 - 18
src/components/RegionPopup/index.tsx

@@ -4,6 +4,8 @@ import MarkIcon from 'assets/icons/mark.svg';
 import EditSvg from 'assets/icons/travels-screens/pen-to-square.svg';
 import CalendarSvg from 'assets/icons/travels-screens/calendar.svg';
 import RotateSvg from 'assets/icons/travels-screens/rotate.svg';
+import CheckSvg from 'assets/icons/travels-screens/circle-check.svg';
+import CheckRegularSvg from 'assets/icons/travels-screens/circle-check-regular.svg';
 
 import { styles } from './style';
 import React from 'react';
@@ -26,6 +28,8 @@ interface RegionPopupProps {
   updateNM: (region: number, first: number, last: number, visits: number, quality: number) => void;
   updateDare: (region: number, visits: 0 | 1) => void;
   disabled?: boolean;
+  updateSlow: (id: number, v: boolean, s11: boolean, s31: boolean, s101: boolean) => void;
+  openEditSlowModal: () => void;
 }
 
 const RegionPopup: React.FC<RegionPopupProps> = ({
@@ -35,7 +39,9 @@ const RegionPopup: React.FC<RegionPopupProps> = ({
   openEditModal,
   updateNM,
   updateDare,
-  disabled
+  disabled,
+  updateSlow,
+  openEditSlowModal
 }) => {
   const fadeAnim = useRef(new Animated.Value(0)).current;
   const navigation = useNavigation();
@@ -71,31 +77,50 @@ const RegionPopup: React.FC<RegionPopupProps> = ({
 
   const formattedCount = formatNumber(region.visitors_count);
 
+  const renderDurationIcon = (condition: 0 | 1) =>
+    condition ? <CheckSvg fill={Colors.DARK_BLUE} /> : <CheckRegularSvg />;
+
   return (
     <Animated.View style={[styles.popupContainer, { opacity: fadeAnim }]}>
       <TouchableOpacity
-        onPress={() =>
-          navigation.navigate(
-            ...([
-              NAVIGATION_PAGES.REGION_PREVIEW,
-              {
-                regionId: region.id,
-                type: userData?.type,
-                disabled
-              }
-            ] as never)
-          )
-        }
+        onPress={() => {
+          if (userData?.type === 'countries') {
+            navigation.navigate(
+              ...([
+                NAVIGATION_PAGES.COUNTRY_PREVIEW,
+                {
+                  regionId: region.id,
+                  type: 'country',
+                  disabled
+                }
+              ] as never)
+            );
+          } else {
+            navigation.navigate(
+              ...([
+                NAVIGATION_PAGES.REGION_PREVIEW,
+                {
+                  regionId: region.id,
+                  type: userData?.type,
+                  disabled
+                }
+              ] as never)
+            );
+          }
+        }}
+        style={{ flex: 1 }}
       >
         <View style={styles.regionInfoContainer}>
           {regionImg && <Image source={{ uri: regionImg }} style={styles.regionImage} />}
           <View style={styles.regionTextContainer}>
             <Text style={styles.regionTitle}>{regionTitle}</Text>
-            <Text style={styles.regionSubtitle}>{regionSubtitle}</Text>
+            {regionSubtitle && <Text style={styles.regionSubtitle}>{regionSubtitle}</Text>}
           </View>
         </View>
 
-        <View style={styles.separator} />
+        <View>
+          <View style={styles.separator} />
+        </View>
 
         <View style={styles.bottomContainer}>
           <View style={styles.userContainer}>
@@ -114,6 +139,23 @@ const RegionPopup: React.FC<RegionPopupProps> = ({
                 </Text>
               </View>
             )}
+            {userData?.visited && userData?.type === 'countries' && !disabled && (
+              <View style={styles.durationContainer}>
+                <View style={styles.durationItem}>
+                  {renderDurationIcon(userData.slow11)}
+                  <Text style={styles.visitDuration}>11+ days</Text>
+                </View>
+                <View style={styles.durationItem}>
+                  {renderDurationIcon(userData.slow31)}
+                  <Text style={styles.visitDuration}>31+ days</Text>
+                </View>
+                <View style={styles.durationItem}>
+                  {renderDurationIcon(userData.slow101)}
+                  <Text style={styles.visitDuration}>101+ days</Text>
+                </View>
+              </View>
+            )}
+
             {(!userData?.visited || userData?.type === 'dare' || disabled) && (
               <View style={styles.userImageContainer}>
                 {userAvatars?.map((avatar, index) => (
@@ -126,8 +168,11 @@ const RegionPopup: React.FC<RegionPopupProps> = ({
             )}
           </View>
           <View style={styles.btnContainer}>
-            {userData?.visited && userData?.type === 'nm' && !disabled ? (
-              <TouchableOpacity onPress={openEditModal} style={styles.editBtn}>
+            {userData?.visited && userData?.type !== 'dare' && !disabled ? (
+              <TouchableOpacity
+                onPress={userData?.type === 'countries' ? openEditSlowModal : openEditModal}
+                style={styles.editBtn}
+              >
                 <EditSvg width={14} height={14} />
               </TouchableOpacity>
             ) : null}
@@ -145,7 +190,15 @@ const RegionPopup: React.FC<RegionPopupProps> = ({
                       userData.visited ? 0 : 1,
                       3
                     )
-                  : updateDare(region.id, userData.visited ? 0 : 1)
+                  : userData?.type === 'countries'
+                    ? updateSlow(
+                        region.id,
+                        !userData.visited,
+                        Boolean(userData.slow11),
+                        Boolean(userData.slow31),
+                        Boolean(userData.slow101)
+                      )
+                    : updateDare(region.id, userData.visited ? 0 : 1)
               }
             >
               {userData?.visited && !disabled ? (

+ 42 - 17
src/components/RegionPopup/style.tsx

@@ -1,5 +1,6 @@
-import { StyleSheet } from 'react-native';
+import { Dimensions, StyleSheet } from 'react-native';
 import { Colors } from '../../theme';
+import { getFontSize } from 'src/utils';
 
 export const styles = StyleSheet.create({
   popupContainer: {
@@ -12,50 +13,50 @@ export const styles = StyleSheet.create({
     borderRadius: 8,
     alignItems: 'center',
     justifyContent: 'center',
-    height: 152,
+    // height: 152,
     marginHorizontal: 24,
     shadowColor: 'rgba(33, 37, 41, 0.12)',
     shadowOffset: { width: 0, height: 4 },
     shadowRadius: 8,
     elevation: 5,
-    zIndex: 2,
+    zIndex: 2
   },
   regionInfoContainer: {
     flexDirection: 'row',
     alignItems: 'center',
     justifyContent: 'flex-start',
-    width: '100%',
+    width: '100%'
   },
   regionImage: {
     width: 60,
     height: 60,
     borderRadius: 6,
-    marginRight: 10,
+    marginRight: 10
   },
   regionTextContainer: {
     justifyContent: 'center',
-    flex: 1,
+    flex: 1
   },
   regionTitle: {
     fontSize: 16,
     fontWeight: 'bold',
-    color: Colors.DARK_BLUE,
+    color: Colors.DARK_BLUE
   },
   regionSubtitle: {
     fontSize: 13,
-    color: Colors.TEXT_GRAY,
+    color: Colors.TEXT_GRAY
   },
   separator: {
     borderBottomWidth: 1,
     borderBottomColor: Colors.DARK_LIGHT,
     width: '100%',
-    marginVertical: 16,
+    marginVertical: 16
   },
   bottomContainer: {
     flexDirection: 'row',
     alignItems: 'center',
     justifyContent: 'space-between',
-    width: '100%',
+    width: '100%'
   },
   userContainer: {
     flexDirection: 'row',
@@ -66,7 +67,7 @@ export const styles = StyleSheet.create({
   userImageContainer: {
     flexDirection: 'row',
     alignItems: 'center',
-    marginRight: 10,
+    marginRight: 10
   },
   userImage: {
     width: 28,
@@ -75,7 +76,7 @@ export const styles = StyleSheet.create({
     marginLeft: -6,
     borderWidth: 1,
     borderColor: Colors.DARK_LIGHT,
-    resizeMode: 'cover',
+    resizeMode: 'cover'
   },
   userCountContainer: {
     width: 28,
@@ -84,12 +85,12 @@ export const styles = StyleSheet.create({
     backgroundColor: Colors.DARK_LIGHT,
     alignItems: 'center',
     justifyContent: 'center',
-    marginLeft: -6,
+    marginLeft: -6
   },
   userCount: {
     fontSize: 12,
     color: Colors.DARK_BLUE,
-    lineHeight: 24,
+    lineHeight: 24
   },
   markVisitedButton: {
     backgroundColor: Colors.ORANGE,
@@ -100,13 +101,14 @@ export const styles = StyleSheet.create({
     color: Colors.WHITE,
     fontWeight: '700',
     letterSpacing: 0.01,
-    lineHeight: 16,
+    lineHeight: 16
   },
   infoContent: { flexDirection: 'row', alignItems: 'center', gap: 4 },
   visitedButtonText: {
     color: Colors.DARK_BLUE,
     fontWeight: 'bold',
-    fontSize: 13
+    fontSize: 13,
+    lineHeight: 16
   },
   btnContainer: {
     alignItems: 'center',
@@ -129,7 +131,8 @@ export const styles = StyleSheet.create({
   markVisitedButtonText: {
     color: 'white',
     fontWeight: 'bold',
-    fontSize: 13
+    fontSize: 13,
+    lineHeight: 16
   },
   visitedContainer: { gap: 6, flexDirection: 'row', alignItems: 'center' },
   editBtn: {
@@ -140,4 +143,26 @@ export const styles = StyleSheet.create({
     justifyContent: 'center',
     padding: 7
   },
+  durationContainer: {
+    flexDirection: 'row',
+    justifyContent: 'center',
+    gap: 8,
+    marginLeft: -6
+  },
+  durationItem: {
+    alignItems: 'center',
+    justifyContent: 'center',
+    gap: 4
+  },
+  durationIconActive: {
+    backgroundColor: Colors.WHITE
+  },
+  durationIconInactive: {
+    backgroundColor: Colors.LIGHT_GRAY
+  },
+  visitDuration: {
+    color: Colors.DARK_BLUE,
+    fontWeight: '600',
+    fontSize: getFontSize(10)
+  }
 });

+ 65 - 2
src/contexts/RegionContext.tsx

@@ -1,7 +1,8 @@
+import { usePostSetSlowMutation } from '@api/countries';
 import { usePostSetDareRegionMutation } from '@api/myDARE';
 import { usePostSetNmRegionMutation } from '@api/myRegions';
 import React, { createContext, useContext, useState, useCallback } from 'react';
-import { DareRegion, NmRegion } from 'src/screens/InAppScreens/TravelsScreen/utils/types';
+import { DareRegion, NmRegion, SlowData } from 'src/screens/InAppScreens/TravelsScreen/utils/types';
 import { StoreType, storage } from 'src/storage';
 
 const RegionContext = createContext<any>(null);
@@ -12,9 +13,11 @@ export const RegionProvider = ({ children }: { children: React.ReactNode }) => {
   const [userData, setUserData] = useState({});
   const [nmRegions, setNmRegions] = useState<NmRegion[]>([]);
   const [dareRegions, setDareRegions] = useState<DareRegion[] | null>([]);
+  const [slow, setSlow] = useState<SlowData[]>([]);
   const token = storage.get('token', StoreType.STRING) as string;
   const { mutate: updateNM } = usePostSetNmRegionMutation();
   const { mutate: updateDARE } = usePostSetDareRegionMutation();
+  const { mutate: updateSlow } = usePostSetSlowMutation();
 
   const handleUpdateNM = useCallback(
     (region: number, first: number, last: number, visits: number, quality: number) => {
@@ -58,6 +61,31 @@ export const RegionProvider = ({ children }: { children: React.ReactNode }) => {
     [userData, token]
   );
 
+  const handleUpdateSlow = useCallback(
+    (id: number, v: boolean, s11: boolean, s31: boolean, s101: boolean) => {
+      const updatedSlow = {
+        type: 'countries',
+        visited: v,
+        slow11: !v ? 0 : (Number(s11) as 0 | 1),
+        slow31: !v ? 0 : (Number(s31) as 0 | 1),
+        slow101: !v ? 0 : (Number(s101) as 0 | 1)
+      };
+
+      const updatedSlowData = {
+        token,
+        id,
+        v,
+        s11: !v ? false : s11,
+        s31: !v ? false : s31,
+        s101: !v ? false : s101
+      };
+
+      updateSlow(updatedSlowData);
+      setUserData(updatedSlow);
+    },
+    [userData, token]
+  );
+
   const handleUpdateNMList = useCallback(
     (id: number, first: number, last: number, visits: number, quality: number) => {
       const updatedNM = nmRegions.map((item) => {
@@ -113,6 +141,37 @@ export const RegionProvider = ({ children }: { children: React.ReactNode }) => {
     [dareRegions]
   );
 
+  const handleUpdateSlowList = useCallback(
+    (id: number, v: boolean, s11: boolean, s31: boolean, s101: boolean) => {
+      const updatedSlow = slow?.map((item) => {
+        if (item.country_id === id) {
+          return {
+            ...item,
+            visited: Number(v) as 0 | 1,
+            slow11: !v ? 0 : (Number(s11) as 0 | 1),
+            slow31: !v ? 0 : (Number(s31) as 0 | 1),
+            slow101: !v ? 0 : (Number(s101) as 0 | 1)
+          };
+        }
+
+        return item;
+      });
+
+      const updatedSlowData = {
+        token,
+        id,
+        v,
+        s11: !v ? false : s11,
+        s31: !v ? false : s31,
+        s101: !v ? false : s101
+      };
+
+      updateSlow(updatedSlowData);
+      updatedSlow && setSlow(updatedSlow);
+    },
+    [slow]
+  );
+
   return (
     <RegionContext.Provider
       value={{
@@ -125,7 +184,11 @@ export const RegionProvider = ({ children }: { children: React.ReactNode }) => {
         handleUpdateNM,
         handleUpdateDare,
         handleUpdateNMList,
-        handleUpdateDareList
+        handleUpdateDareList,
+        handleUpdateSlow,
+        handleUpdateSlowList,
+        slow,
+        setSlow
       }}
     >
       {children}

+ 47 - 0
src/db/index.ts

@@ -7,8 +7,10 @@ import { fetchLastDareDbUpdate, fetchLastRegionsDbUpdate } from '@api/app';
 
 let db1: SQLite.SQLiteDatabase | null = null;
 let db2: SQLite.SQLiteDatabase | null = null;
+let db3: SQLite.SQLiteDatabase | null = null;
 const nmRegionsDBname = 'nmRegions.db';
 const darePlacesDBname = 'darePlaces.db';
+const countriesDBname = 'nmCountries.db';
 const sqliteDirectory = 'SQLite';
 const sqliteFullPath = FileSystem.documentDirectory + sqliteDirectory;
 const DS = '/';
@@ -69,9 +71,27 @@ export async function openDatabases() {
       );
     }
 
+    const countriesDB = await FileSystem.getInfoAsync(sqliteFullPath + DS + countriesDBname, {
+      size: true
+    });
+    if (!countriesDB.exists) {
+      await copyDatabaseFile(
+        countriesDBname,
+        Asset.fromModule(require('../../assets/db/' + countriesDBname))
+      );
+    }
+    if (countriesDB.exists && countriesDB.size == 0) {
+      await FileSystem.deleteAsync(sqliteFullPath + DS + countriesDBname);
+      await copyDatabaseFile(
+        countriesDBname,
+        Asset.fromModule(require('../../assets/db/' + countriesDBname))
+      );
+    }
+
     const openDatabase = (dbName: string) => SQLite.openDatabase(dbName);
     db1 = openDatabase(nmRegionsDBname);
     db2 = openDatabase(darePlacesDBname);
+    db3 = openDatabase(countriesDBname);
   } catch (error) {
     console.error('openDatabases - Error:');
     console.error(JSON.stringify(error, null, 2));
@@ -86,6 +106,7 @@ export async function refreshDatabases() {
     }
     await refreshNmDatabase();
     await refreshDarePlacesDatabase();
+    await refreshCountriesDatabase();
   } catch (error) {
     console.error('refreshDatabases - Error:');
     console.error(JSON.stringify(error, null, 2));
@@ -100,6 +121,10 @@ export function getSecondDatabase() {
   return db2;
 }
 
+export function getCountriesDatabase() {
+  return db3;
+}
+
 const openDatabase = (dbName: string) => SQLite.openDatabase(dbName);
 
 async function refreshNmDatabase() {
@@ -142,6 +167,28 @@ async function refreshDarePlacesDatabase() {
   }
 }
 
+async function refreshCountriesDatabase() {
+  try {
+    await FileSystem.deleteAsync(sqliteFullPath + DS + countriesDBname, { idempotent: true });
+    const countriesUrl = `${API_HOST}/static/app/${countriesDBname}`;
+    let countriesFileUri = sqliteFullPath + DS + countriesDBname;
+
+    const countriesResponse = await FileSystem.downloadAsync(countriesUrl, countriesFileUri);
+
+    if (countriesResponse.status !== 200) {
+      throw new Error(
+        `Failed to download the countriesDb file: Status code ${countriesResponse.status}`
+      );
+    }
+
+    db3 = null;
+    db3 = openDatabase(countriesDBname);
+  } catch (error) {
+    console.error('refreshDatabase nmCountries - Error:');
+    console.error(JSON.stringify(error, null, 2));
+  }
+}
+
 export async function updateNmRegionsDb(localLastDate: string) {
   const lastUpdate = await fetchLastRegionsDbUpdate(localLastDate);
 

+ 96 - 1
src/modules/api/countries/countries-api.tsx

@@ -1,6 +1,7 @@
 import { request } from '../../../utils';
 import { API } from '../../../types';
 import { ResponseType } from '../response-type';
+import { User } from '@api/regions';
 
 export interface PostGetSlowReturn extends ResponseType {
   slow: {
@@ -29,7 +30,101 @@ export interface PostSetSlow {
   s101: boolean;
 }
 
+export interface PostGetCountryDataReturn extends ResponseType {
+  data: {
+    visited: boolean;
+    slow11: 0 | 1;
+    slow31: 0 | 1;
+    slow101: 0 | 1;
+    name: string;
+    flag: string;
+    users_from_country_count: number;
+    users_who_visited_country_count: number;
+    photos: {
+      title: string;
+      id: number;
+      user_id: number;
+      first_name: string;
+      last_name: string;
+      avatar: string | null;
+    }[];
+    users_who_visited_country: string[];
+    users_from_country: string[];
+    series: {
+      series_id: number;
+      series_name: string;
+      icon: string;
+      items: {
+        id: number;
+        name: string;
+        description: string | null;
+        new: 0 | 1;
+        visited: 0 | 1;
+        double_point: 0 | 1;
+        visited_double: 0 | 1;
+        link: string;
+      }[];
+    }[];
+  };
+}
+
+export interface PostGetUsersFromCountryDataReturn extends ResponseType {
+  data: {
+    max_pages: number;
+    users: User[];
+  };
+}
+
+export interface PostGetUsersWhoVisitedCountryDataReturn extends ResponseType {
+  data: {
+    max_pages: number;
+    countries: {
+      [key: string]: {
+        country: string;
+        flag: string;
+      };
+    };
+    users: User[];
+  };
+}
+
+export interface PostGetUserDataReturn extends ResponseType {
+  data: {
+    bbox: any;
+    center: [number, number];
+    visited: boolean;
+    slow11: 0 | 1;
+    slow31: 0 | 1;
+    slow101: 0 | 1;
+  };
+}
+
 export const countriesApi = {
   getSlow: (token: string) => request.postForm<PostGetSlowReturn>(API.GET_SLOW, { token }),
-  setSlow: (data: PostSetSlow) => request.postForm<ResponseType>(API.SET_SLOW, data)
+  setSlow: (data: PostSetSlow) => request.postForm<ResponseType>(API.SET_SLOW, data),
+  getCountryData: (id: number, token?: string) =>
+    request.postForm<PostGetCountryDataReturn>(API.GET_COUNTRY_SCREEN_DATA, { id, token }),
+  getUsersFromCountry: (id: number, page: number, sort?: string, age?: number) =>
+    request.postForm<PostGetUsersFromCountryDataReturn>(API.GET_USERS_FROM_COUNTRY, {
+      id,
+      page,
+      sort,
+      age
+    }),
+  getUsersWhoVisitedCountry: (
+    id: number,
+    page: number,
+    sort?: string,
+    age?: number,
+    country?: string
+  ) =>
+    request.postForm<PostGetUsersWhoVisitedCountryDataReturn>(API.GET_USERS_WHO_VISITED_COUNTRY, {
+      id,
+      page,
+      sort,
+      age,
+      country
+    }),
+  getCountryUserData: (id: number, token: string) =>
+    request.postForm<PostGetUserDataReturn>(API.GET_COUNTRY_USER_DATA, { id, token })
 };

+ 5 - 1
src/modules/api/countries/countries-query-keys.tsx

@@ -1,4 +1,8 @@
 export const countriesQueryKeys = {
   getSlow: (token: string) => ['getSlow', { token }] as const,
-  setSlow: () => ['setSlow'] as const
+  setSlow: () => ['setSlow'] as const,
+  getCountryData: (id: number, token?: string) => ['getCountryData', id, token] as const,
+  getUsersFromCountry: () => ['getUsersFromCountry'] as const,
+  getUsersWhoVisitedCountry: () => ['getUsersWhoVisitedCountry'] as const,
+  getCountryUserData: () => ['getCountryUserData'] as const
 };

+ 4 - 0
src/modules/api/countries/queries/index.ts

@@ -1,2 +1,6 @@
 export * from './use-post-get-slow';
 export * from './use-post-set-slow';
+export * from './use-post-get-country-screen-data';
+export * from './use-post-get-users-from-country';
+export * from './use-post-get-users-who-visited-country';
+export * from './use-post-get-user-data-country-app';

+ 17 - 0
src/modules/api/countries/queries/use-post-get-country-screen-data.tsx

@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { countriesQueryKeys } from '../countries-query-keys';
+import { countriesApi, type PostGetCountryDataReturn } from '../countries-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const useGetCountryDataQuery = (id: number, enabled: boolean, token?: string) => {
+  return useQuery<PostGetCountryDataReturn, BaseAxiosError>({
+    queryKey: countriesQueryKeys.getCountryData(id, token),
+    queryFn: async () => {
+      const response = await countriesApi.getCountryData(id, token);
+      return response.data;
+    },
+    enabled
+  });
+};

+ 21 - 0
src/modules/api/countries/queries/use-post-get-user-data-country-app.tsx

@@ -0,0 +1,21 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { countriesQueryKeys } from '../countries-query-keys';
+import { countriesApi, type PostGetUserDataReturn } from '../countries-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const fetchCountryUserData = () => {
+  return useMutation<
+    PostGetUserDataReturn,
+    BaseAxiosError,
+    { token: string; id: number },
+    PostGetUserDataReturn
+  >({
+    mutationKey: countriesQueryKeys.getCountryUserData(),
+    mutationFn: async (variables) => {
+      const response = await countriesApi.getCountryUserData(variables.id, variables.token);
+      return response.data;
+    }
+  });
+};

+ 26 - 0
src/modules/api/countries/queries/use-post-get-users-from-country.tsx

@@ -0,0 +1,26 @@
+import { countriesQueryKeys } from '../countries-query-keys';
+import { countriesApi, type PostGetUsersFromCountryDataReturn } from '../countries-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+import { useMutation } from '@tanstack/react-query';
+
+export const useGetUsersFromCountryMutation = () => {
+  return useMutation<
+    PostGetUsersFromCountryDataReturn,
+    BaseAxiosError,
+    { id: number; page: number; sort?: string; age?: number },
+    PostGetUsersFromCountryDataReturn
+  >({
+    mutationKey: countriesQueryKeys.getUsersFromCountry(),
+    mutationFn: async (variables) => {
+      const response = await countriesApi.getUsersFromCountry(
+        variables.id,
+        variables.page,
+        variables.sort,
+        variables.age
+      );
+      return response.data;
+    }
+  });
+};

+ 27 - 0
src/modules/api/countries/queries/use-post-get-users-who-visited-country.tsx

@@ -0,0 +1,27 @@
+import { countriesQueryKeys } from '../countries-query-keys';
+import { countriesApi, type PostGetUsersWhoVisitedCountryDataReturn } from '../countries-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+import { useMutation } from '@tanstack/react-query';
+
+export const useGetUsersWhoVisitedCountryMutation = () => {
+  return useMutation<
+    PostGetUsersWhoVisitedCountryDataReturn,
+    BaseAxiosError,
+    { id: number; page: number; sort?: string; age?: number; country?: string },
+    PostGetUsersWhoVisitedCountryDataReturn
+  >({
+    mutationKey: countriesQueryKeys.getUsersWhoVisitedCountry(),
+    mutationFn: async (variables) => {
+      const response = await countriesApi.getUsersWhoVisitedCountry(
+        variables.id,
+        variables.page,
+        variables.sort,
+        variables.age,
+        variables.country
+      );
+      return response.data;
+    }
+  });
+};

+ 492 - 0
src/screens/InAppScreens/MapScreen/CountryViewScreen/index.tsx

@@ -0,0 +1,492 @@
+import React, { FC, useCallback, useEffect, useState } from 'react';
+import { View, Text, Image, TouchableOpacity, Platform } from 'react-native';
+import ImageView from 'better-react-native-image-viewing';
+import { styles } from '../RegionViewScreen/styles';
+import { Button, HorizontalTabView, Loading, Modal as ReactModal } from 'src/components';
+import { useFocusEffect } from '@react-navigation/native';
+import { Colors } from 'src/theme';
+import { ScrollView } from 'react-native-gesture-handler';
+
+import { styles as ButtonStyles } from 'src/components/RegionPopup/style';
+import { usePostSetToggleItem } from '@api/series';
+import { NAVIGATION_PAGES } from 'src/types';
+import { API_HOST } from 'src/constants';
+import { StoreType, storage } from 'src/storage';
+import { ButtonVariants } from 'src/types/components';
+import { useRegion } from 'src/contexts/RegionContext';
+import formatNumber from '../../TravelsScreen/utils/formatNumber';
+import { PhotosData, Props, SeriesData, SeriesGroup, SeriesItem } from '../RegionViewScreen/types';
+import ImageCarousel from '../RegionViewScreen/ImageCarousel';
+import TravelSeriesList from '../RegionViewScreen/TravelSeriesList';
+import { useGetCountryDataQuery } from '@api/countries';
+import EditModal from '../../TravelsScreen/Components/EditSlowModal';
+
+import MarkIcon from 'assets/icons/mark.svg';
+import ChevronLeft from 'assets/icons/chevron-left.svg';
+import CaseSvg from 'assets/icons/briefcase.svg';
+import HouseSvg from 'assets/icons/house.svg';
+import EditSvg from 'assets/icons/travels-screens/pen-to-square.svg';
+import CheckSvg from 'assets/icons/travels-screens/circle-check.svg';
+import CheckRegularSvg from 'assets/icons/travels-screens/circle-check-regular.svg';
+
+const CountryViewScreen: FC<Props> = ({ navigation, route }) => {
+  const countryId = route.params?.regionId;
+  const disabled = route.params?.disabled;
+  const token = storage.get('token', StoreType.STRING) as string;
+  const [isLoading, setIsLoading] = useState(true);
+  const [isModalVisible, setModalVisible] = useState(false);
+  const [currentImageIndex, setCurrentImageIndex] = useState(0);
+  const [activeIndex, setActiveIndex] = useState(0);
+  const [indexSeries, setIndexSeries] = useState(0);
+  const [routes, setRoutes] = useState<SeriesGroup[]>([]);
+  const [series, setSeries] = useState<SeriesData[]>([]);
+  const [photos, setPhotos] = useState<PhotosData[]>([]);
+  const [name, setName] = useState('');
+  const { data } = useGetCountryDataQuery(countryId, true, token && token);
+  const { mutate: updateSeriesItem } = usePostSetToggleItem();
+  const [isInfoModalVisible, setIsInfoModalVisible] = useState<boolean>(false);
+  const [infoItem, setInfoItem] = useState<SeriesItem | null>(null);
+  const [isEditSlowModalVisible, setIsEditSlowModalVisible] = useState<boolean>(false);
+
+  const {
+    handleUpdateSlow: updateSlow,
+    userData: regionData,
+    setUserData: setRegionData,
+    handleUpdateSlowList
+  } = useRegion();
+
+  useEffect(() => {
+    navigation.getParent()?.setOptions({
+      tabBarStyle: {
+        display: 'none',
+        position: 'absolute',
+        ...Platform.select({
+          android: {
+            height: 58
+          }
+        })
+      }
+    });
+  }, [navigation]);
+
+  useFocusEffect(
+    useCallback(() => {
+      const fetchGroups = async () => {
+        let staticGroups = [
+          {
+            key: 'all',
+            title: 'All',
+            series_id: 0
+          }
+        ];
+        const routesData = data?.data?.series?.map((item) => ({
+          key: item.series_id?.toString(),
+          title: item.series_name,
+          series_id: item.series_id,
+          icon: item.icon,
+          items: item.items
+        }));
+
+        routesData && staticGroups.push(...routesData);
+
+        setPhotos(
+          data?.data?.photos?.map((item) => ({
+            ...item,
+            uriSmall: `${API_HOST}/ajax/pic/${item.id}/small`,
+            uri: `${API_HOST}/ajax/pic/${item.id}/full`
+          })) ?? []
+        );
+
+        setName(data?.data.name ?? '');
+
+        setSeries(data?.data?.series || []);
+        setRoutes(staticGroups);
+        setIsLoading(false);
+      };
+
+      if (data && data.result === 'OK') {
+        fetchGroups();
+      }
+    }, [data])
+  );
+
+  const handleCheckboxChange = useCallback(
+    async (item: SeriesItem, double: boolean, seriesId: number) => {
+      setSeries((currentData) => {
+        const groupIndex = currentData.findIndex((group) => group?.series_id === seriesId);
+
+        if (groupIndex === -1) return currentData;
+
+        const newData = [...currentData];
+        const newGroup = { ...newData[groupIndex] };
+
+        newGroup.items = newGroup.items.map((subItem) =>
+          subItem.id === item.id
+            ? {
+                ...subItem,
+                ...(double
+                  ? { visited_double: subItem.visited_double === 0 ? 1 : 0 }
+                  : { visited: subItem.visited === 0 ? 1 : 0 })
+              }
+            : subItem
+        );
+
+        newData[groupIndex] = newGroup;
+        return newData;
+      });
+
+      const itemData = {
+        token: token,
+        series_id: seriesId,
+        item_id: item.id,
+        checked: (item.visited === 1 ? 0 : 1) as 0 | 1,
+        double: (double && !item.visited_double ? 1 : 0) as 0 | 1
+      };
+
+      try {
+        updateSeriesItem(itemData);
+      } catch (error) {
+        console.error('Failed to update checkbox state', error);
+      }
+    },
+    [token, updateSeriesItem]
+  );
+
+  const openModal = (index: number) => {
+    setCurrentImageIndex(index);
+    setModalVisible(true);
+  };
+
+  if (isLoading) return <Loading />;
+
+  const handleUpdateSlowModal = (
+    id: number,
+    v: boolean,
+    s11: boolean,
+    s31: boolean,
+    s101: boolean
+  ) => {
+    const updatedSlow = {
+      ...regionData,
+      visited: v,
+      slow11: Number(s11) as 0 | 1,
+      slow31: Number(s31) as 0 | 1,
+      slow101: Number(s101) as 0 | 1
+    };
+
+    const updatedSlowData = {
+      token,
+      id,
+      v,
+      s11,
+      s31,
+      s101
+    };
+
+    route.params?.isTravelsScreen
+      ? handleUpdateSlowList(id, v, s11, s31, s101)
+      : updateSlow(id, v, s11, s31, s101);
+    updatedSlow && setRegionData(updatedSlow);
+  };
+
+  const handleUpdateSlow = () => {
+    route.params?.isTravelsScreen
+      ? handleUpdateSlowList(
+          countryId,
+          !regionData.visited,
+          Boolean(regionData.slow11),
+          Boolean(regionData.slow31),
+          Boolean(regionData.slow101)
+        )
+      : updateSlow(
+          countryId,
+          !regionData.visited,
+          Boolean(regionData.slow11),
+          Boolean(regionData.slow31),
+          Boolean(regionData.slow101)
+        );
+
+    setRegionData({
+      ...regionData,
+      visited: !regionData.visited
+    });
+  };
+
+  const renderDurationIcon = (condition: 0 | 1) =>
+    condition ? <CheckSvg fill={Colors.DARK_BLUE} /> : <CheckRegularSvg />;
+
+  return (
+    <View style={styles.container}>
+      <TouchableOpacity
+        onPress={() => {
+          navigation.goBack();
+        }}
+        style={styles.backButton}
+      >
+        <View style={styles.chevronWrapper}>
+          <ChevronLeft fill={Colors.WHITE} />
+        </View>
+      </TouchableOpacity>
+      <ScrollView
+        contentContainerStyle={{ flexGrow: 1 }}
+        nestedScrollEnabled={true}
+        showsVerticalScrollIndicator={false}
+      >
+        {photos.length > 0 ? (
+          <ImageCarousel
+            photos={photos}
+            activeIndex={activeIndex}
+            setActiveIndex={setActiveIndex}
+            openModal={openModal}
+          />
+        ) : (
+          <View style={styles.emptyImage}>
+            <Image
+              source={require('../../../../../assets/images/logo-opacity.png')}
+              style={{ width: 100, height: 100 }}
+            />
+            <Text style={styles.emptyImageText}>No image available at this location</Text>
+          </View>
+        )}
+
+        <View style={styles.wrapper}>
+          {regionData?.visited && !disabled && (
+            <View style={styles.durationContainer}>
+              <View style={styles.durationItem}>
+                {renderDurationIcon(regionData.slow11)}
+                <Text style={styles.visitDuration}>11+ days</Text>
+              </View>
+              <View style={styles.durationItem}>
+                {renderDurationIcon(regionData.slow31)}
+                <Text style={styles.visitDuration}>31+ days</Text>
+              </View>
+              <View style={styles.durationItem}>
+                {renderDurationIcon(regionData.slow101)}
+                <Text style={styles.visitDuration}>101+ days</Text>
+              </View>
+            </View>
+          )}
+          <View style={styles.nameContainer}>
+            <Text style={styles.title}>{name}</Text>
+            <View style={ButtonStyles.btnContainer}>
+              {regionData?.visited && !disabled ? (
+                <TouchableOpacity
+                  onPress={() => setIsEditSlowModalVisible(true)}
+                  style={ButtonStyles.editBtn}
+                >
+                  <EditSvg width={14} height={14} />
+                </TouchableOpacity>
+              ) : null}
+              {!disabled ? (
+                <TouchableOpacity
+                  style={[
+                    ButtonStyles.btn,
+                    regionData?.visited && !disabled
+                      ? ButtonStyles.visitedButton
+                      : ButtonStyles.markVisitedButton
+                  ]}
+                  onPress={handleUpdateSlow}
+                >
+                  {regionData?.visited ? (
+                    <View style={ButtonStyles.visitedContainer}>
+                      <MarkIcon width={16} height={16} />
+                      <Text style={ButtonStyles.visitedButtonText}>Visited</Text>
+                    </View>
+                  ) : (
+                    <Text style={[ButtonStyles.markVisitedButtonText]}>Mark Visited</Text>
+                  )}
+                </TouchableOpacity>
+              ) : null}
+            </View>
+          </View>
+
+          <View style={styles.divider} />
+
+          <View style={styles.stats}>
+            {data?.data.users_from_country_count ?? 0 > 0 ? (
+              <TouchableOpacity
+                style={[styles.statItem, { justifyContent: 'flex-start' }]}
+                onPress={() =>
+                  navigation.navigate(
+                    ...([
+                      NAVIGATION_PAGES.USERS_LIST,
+                      {
+                        id: countryId,
+                        isFromHere: true,
+                        type: 'country'
+                      }
+                    ] as never)
+                  )
+                }
+              >
+                <View style={styles.icon}>
+                  <HouseSvg />
+                </View>
+                <View
+                  style={{
+                    height: 12,
+                    width: 1,
+                    backgroundColor: Colors.DARK_BLUE,
+                    marginRight: 6
+                  }}
+                />
+                <View style={styles.userImageContainer}>
+                  {data?.data.users_from_country &&
+                    data?.data.users_from_country.length > 0 &&
+                    data?.data.users_from_country?.map((user, index: number) => (
+                      <Image
+                        key={index}
+                        source={{ uri: API_HOST + user }}
+                        style={styles.userImage}
+                      />
+                    ))}
+                  <View style={styles.userCountContainer}>
+                    <Text style={styles.userCount}>
+                      {formatNumber(data?.data?.users_from_country_count ?? 0)}
+                    </Text>
+                  </View>
+                </View>
+              </TouchableOpacity>
+            ) : (
+              <View style={[styles.statItem, { justifyContent: 'flex-start' }]} />
+            )}
+
+            {data?.data.users_who_visited_country_count ?? 0 > 0 ? (
+              <TouchableOpacity
+                style={[styles.statItem, { justifyContent: 'flex-end' }]}
+                onPress={() =>
+                  navigation.navigate(
+                    ...([
+                      NAVIGATION_PAGES.USERS_LIST,
+                      {
+                        id: countryId,
+                        isFromHere: false,
+                        type: 'country'
+                      }
+                    ] as never)
+                  )
+                }
+              >
+                <View style={styles.icon}>
+                  <CaseSvg />
+                </View>
+                <View
+                  style={{
+                    height: 12,
+                    width: 1,
+                    backgroundColor: Colors.DARK_BLUE,
+                    marginRight: 6
+                  }}
+                />
+
+                <View style={styles.userImageContainer}>
+                  {data?.data.users_who_visited_country &&
+                    data?.data.users_who_visited_country.length > 0 &&
+                    data?.data.users_who_visited_country?.map((user, index) => (
+                      <Image
+                        key={index}
+                        source={{ uri: API_HOST + user }}
+                        style={[styles.userImage]}
+                      />
+                    ))}
+                  <View style={styles.userCountContainer}>
+                    <Text style={styles.userCount}>
+                      {formatNumber(data?.data.users_who_visited_country_count ?? 0)}
+                    </Text>
+                  </View>
+                </View>
+              </TouchableOpacity>
+            ) : (
+              <View style={[styles.statItem, { justifyContent: 'flex-end' }]} />
+            )}
+          </View>
+
+          <View style={[styles.divider, { marginBottom: 8 }]} />
+
+          {series.length > 0 ? (
+            <>
+              <Text style={styles.travelSeriesTitle}>TRAVEL SERIES</Text>
+              <HorizontalTabView
+                index={indexSeries}
+                setIndex={setIndexSeries}
+                routes={routes}
+                renderScene={({ route }: { route: SeriesGroup }) => <View style={{ height: 0 }} />}
+              />
+              <TravelSeriesList
+                series={series}
+                indexSeries={indexSeries}
+                routes={routes}
+                handleCheckboxChange={handleCheckboxChange}
+                setIsInfoModalVisible={setIsInfoModalVisible}
+                setInfoItem={setInfoItem}
+                disabled={disabled}
+              />
+            </>
+          ) : null}
+        </View>
+
+        <ImageView
+          images={photos}
+          imageIndex={currentImageIndex}
+          visible={isModalVisible}
+          onRequestClose={() => setModalVisible(false)}
+          backgroundColor={Colors.DARK_BLUE}
+          onImageIndexChange={setActiveIndex}
+          FooterComponent={({ imageIndex }) => (
+            <View style={styles.imageFooter}>
+              <Text style={styles.imageDescription}>{photos[imageIndex].title}</Text>
+              <TouchableOpacity
+                onPress={() => {
+                  setModalVisible(false);
+                  navigation.navigate(
+                    ...([
+                      NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW,
+                      { userId: photos[imageIndex].user_id, hideTabBar: true }
+                    ] as never)
+                  );
+                }}
+                disabled={disabled}
+                style={styles.imageOwner}
+              >
+                <Text style={styles.imageOwnerText}>{photos[imageIndex].first_name}</Text>
+                <Text style={styles.imageOwnerText}>{photos[imageIndex].last_name}</Text>
+              </TouchableOpacity>
+            </View>
+          )}
+        />
+
+        <ReactModal
+          visible={isInfoModalVisible}
+          children={
+            <View style={styles.modalView}>
+              <Text style={styles.infoTitle}>{infoItem?.name}</Text>
+              <Text style={styles.infoText}>{infoItem?.description}</Text>
+              <Button
+                variant={ButtonVariants.OPACITY}
+                containerStyles={styles.btnContainer}
+                textStyles={{
+                  color: Colors.DARK_BLUE
+                }}
+                onPress={() => setIsInfoModalVisible(false)}
+                children={'Got it'}
+              />
+            </View>
+          }
+          onRequestClose={() => setIsInfoModalVisible(false)}
+          headerTitle={'Info'}
+          visibleInPercent={'auto'}
+        />
+      </ScrollView>
+      <EditModal
+        isVisible={isEditSlowModalVisible}
+        onClose={() => setIsEditSlowModalVisible(false)}
+        item={{ ...regionData, country_id: countryId }}
+        updateSlow={(id, v, s11, s31, s101) => {
+          handleUpdateSlowModal(id, v, s11, s31, s101);
+        }}
+      />
+    </View>
+  );
+};
+
+export default CountryViewScreen;

+ 22 - 0
src/screens/InAppScreens/MapScreen/RegionViewScreen/styles.tsx

@@ -1,5 +1,6 @@
 import { StyleSheet } from 'react-native';
 import { Colors } from '../../../../theme';
+import { getFontSize } from 'src/utils';
 
 export const styles = StyleSheet.create({
   container: {
@@ -169,5 +170,26 @@ export const styles = StyleSheet.create({
     color: Colors.DARK_BLUE,
     fontWeight: 'bold',
     fontSize: 13
+  },
+  durationContainer: {
+    flexDirection: 'row',
+    justifyContent: 'center',
+    gap: 12
+  },
+  durationItem: {
+    alignItems: 'center',
+    justifyContent: 'center',
+    gap: 4
+  },
+  durationIconActive: {
+    backgroundColor: Colors.WHITE
+  },
+  durationIconInactive: {
+    backgroundColor: Colors.LIGHT_GRAY
+  },
+  visitDuration: {
+    color: Colors.DARK_BLUE,
+    fontWeight: '600',
+    fontSize: getFontSize(11)
   }
 });

+ 4 - 1
src/screens/InAppScreens/MapScreen/UniversalSearch/index.tsx

@@ -51,7 +51,10 @@ const SearchModal = ({
           } else {
             handleCloseModal();
             navigation.navigate(
-              ...([NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW, { userId: item.id }] as never)
+              ...([
+                NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW,
+                { userId: item.id, hideTabBar: true }
+              ] as never)
             );
           }
         }}

+ 74 - 3
src/screens/InAppScreens/MapScreen/UsersListScreen/index.tsx

@@ -17,6 +17,7 @@ import { RankingDropdown } from '../../TravellersScreen/utils/types';
 import { dataRanking } from '../../TravellersScreen/utils';
 import { Ranking } from '../../TravellersScreen';
 import { useGetFriendsMutation } from '@api/friends';
+import { useGetUsersFromCountryMutation, useGetUsersWhoVisitedCountryMutation } from '@api/countries';
 
 type Props = {
   navigation: NavigationProp<any>;
@@ -29,8 +30,9 @@ const UsersListScreen: FC<Props> = ({ navigation, route }) => {
   const { mutateAsync: getUsersFromRegion } = useGetUsersFromRegionMutation();
   const { mutateAsync: getUsersWhoVisitedRegion } = useGetUsersWhoVisitetRegionMutation();
   const { mutateAsync: getUsersWhoVisitedDare } = useGetUsersWhoVisitedDareMutation();
+  const { mutateAsync: getUsersFromCountry } = useGetUsersFromCountryMutation();
+  const { mutateAsync: getUsersWhoVisitedCountry } = useGetUsersWhoVisitedCountryMutation();
   const { mutateAsync: getFriends } = useGetFriendsMutation();
-  const token = storage.get('token', StoreType.STRING);
   const [users, setUsers] = useState<any[]>([]);
   const [loading, setLoading] = useState(true);
   const [selectedUsers, setSelectedUsers] = useState<any[]>([]);
@@ -58,7 +60,7 @@ const UsersListScreen: FC<Props> = ({ navigation, route }) => {
 
   useEffect(() => {
     const getNextPage = async () => {
-      if (isFromHere) {
+      if (isFromHere && type === 'nm') {
         await getUsersFromRegion(
           {
             id,
@@ -108,6 +110,39 @@ const UsersListScreen: FC<Props> = ({ navigation, route }) => {
             }
           }
         );
+      } else if (isFromHere && type === 'country') {
+        await getUsersFromCountry(
+          {
+            id,
+            page,
+            sort: filter.ranking,
+            age: filter.age
+          },
+          {
+            onSuccess: (data) => {
+              setIsLoadingMore(false);
+              setUsers((prevState) => [...prevState, ...data?.data?.users]);
+              setSelectedUsers((prevState) => [...prevState, ...data?.data?.users]);
+            }
+          }
+        );
+      } else if (type === 'country') {
+        await getUsersWhoVisitedCountry(
+          {
+            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(
           {
@@ -133,7 +168,7 @@ const UsersListScreen: FC<Props> = ({ navigation, route }) => {
   }, [page]);
 
   const applySort = async () => {
-    if (isFromHere) {
+    if (isFromHere && type === 'nm') {
       await getUsersFromRegion(
         {
           id,
@@ -169,6 +204,42 @@ const UsersListScreen: FC<Props> = ({ navigation, route }) => {
           }
         }
       );
+    } else if (isFromHere && type === 'country') {
+      await getUsersFromCountry(
+        {
+          id,
+          page,
+          sort: filter.ranking,
+          age: filter.age
+        },
+        {
+          onSuccess: (data) => {
+            setUsers(data?.data?.users);
+            setSelectedUsers(data?.data?.users);
+            setMaxPages(data?.data?.max_pages);
+            setLoading(false);
+          }
+        }
+      );
+    } else if (type === 'country') {
+      await getUsersWhoVisitedCountry(
+        {
+          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 if (type === 'friends') {
       await getFriends(
         {

+ 72 - 3
src/screens/InAppScreens/MapScreen/index.tsx

@@ -24,11 +24,17 @@ import FilterIcon from 'assets/icons/filter.svg';
 import regions from '../../../../assets/geojson/nm2022.json';
 import jsonData, { fetchJsonData } from '../../../database/geojsonService';
 
-import { getFirstDatabase, getSecondDatabase, refreshDatabases } from '../../../db';
+import {
+  getCountriesDatabase,
+  getFirstDatabase,
+  getSecondDatabase,
+  refreshDatabases
+} from '../../../db';
 import { LocationPopup, WarningModal, EditNmModal } from '../../../components';
 
 import { styles } from './style';
 import {
+  calculateMapCountry,
   calculateMapRegion,
   filterCandidates,
   filterCandidatesMarkers,
@@ -58,6 +64,8 @@ import { NAVIGATION_PAGES } from 'src/types';
 import { useRegion } from 'src/contexts/RegionContext';
 import { useFocusEffect } from '@react-navigation/native';
 import { openstreetmapUrl } from 'src/constants/constants';
+import { fetchCountryUserData } from '@api/countries';
+import EditModal from '../TravelsScreen/Components/EditSlowModal';
 
 const localTileDir = `${FileSystem.cacheDirectory}tiles/background`;
 const localGridDir = `${FileSystem.cacheDirectory}tiles/grid`;
@@ -79,6 +87,7 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
   const { mutateAsync } = fetchSeriesData();
   const { mutateAsync: mutateUserData } = fetchUserData();
   const { mutateAsync: mutateUserDataDare } = fetchUserDataDare();
+  const { mutateAsync: mutateCountriesData } = fetchCountryUserData();
   const { mutate: updateSeriesItem } = usePostSetToggleItem();
   const visitedTiles = `${FASTEST_MAP_HOST}/tiles_nm/user_visited/${userId}`;
   const visitedUNTiles = `${FASTEST_MAP_HOST}/tiles_nm/user_visited_un/${userId}`;
@@ -93,6 +102,7 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
   const [askLocationVisible, setAskLocationVisible] = useState<boolean>(false);
   const [openSettingsVisible, setOpenSettingsVisible] = useState<boolean>(false);
   const [isWarningModalVisible, setIsWarningModalVisible] = useState<boolean>(false);
+  const [isEditSlowModalVisible, setIsEditSlowModalVisible] = useState<boolean>(false);
 
   const [markers, setMarkers] = useState<ItemSeries[]>([]);
   const [series, setSeries] = useState<Series[] | null>(null);
@@ -118,7 +128,7 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
   ];
   const [type, setType] = useState(0);
 
-  const { handleUpdateNM, handleUpdateDare, userData, setUserData } = useRegion();
+  const { handleUpdateNM, handleUpdateDare, handleUpdateSlow, userData, setUserData } = useRegion();
 
   useFocusEffect(
     useCallback(() => {
@@ -378,8 +388,55 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
 
     let db = getSecondDatabase();
     let tableName = 'places';
+    let foundRegion: any;
+
+    type !== 1 && (foundRegion = findRegionInDataset(dareData, point));
+
+    if (type === 1) {
+      foundRegion = findRegionInDataset(regions, point);
+      db = getFirstDatabase();
+      tableName = 'regions';
+
+      if (foundRegion) {
+        const countryId = foundRegion.properties?.country_id;
+
+        if (countryId) {
+          if (countryId === regionData?.id) return;
+
+          setSelectedRegion(null);
+          setMarkers([]);
+          setProcessedMarkers([]);
+
+          db = getCountriesDatabase();
+          tableName = 'countries';
 
-    let foundRegion = findRegionInDataset(dareData, point);
+          await getData(db, countryId, tableName, handleRegionData)
+            .then(() => {
+              setRegionPopupVisible(true);
+            })
+            .catch((error) => {
+              console.error('Error fetching data', error);
+              refreshDatabases();
+            });
+
+          await mutateCountriesData(
+            { id: +countryId, token },
+            {
+              onSuccess: (data) => {
+                setUserData({ type: 'countries', ...data.data });
+                const bounds = turf.bbox(data.data.bbox);
+                const center = data.data.center;
+                const region = calculateMapCountry(bounds, center);
+
+                mapRef.current?.animateToRegion(region, 1000);
+              }
+            }
+          );
+
+          return;
+        }
+      }
+    }
 
     if (!foundRegion) {
       foundRegion = findRegionInDataset(regions, point);
@@ -665,6 +722,10 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
     handlePress();
   };
 
+  const handleOpenEditSlowModal = () => {
+    setIsEditSlowModalVisible(true);
+  };
+
   return (
     <View style={styles.container}>
       <ClusteredMapView
@@ -754,6 +815,8 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
               handleUpdateDare(id, visits);
             }}
             disabled={!token || !isConnected}
+            updateSlow={handleUpdateSlow}
+            openEditSlowModal={handleOpenEditSlowModal}
           />
         </>
       ) : (
@@ -854,6 +917,12 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation }) => {
         type={type}
         setType={setType}
       />
+      <EditModal
+        isVisible={isEditSlowModalVisible}
+        onClose={() => setIsEditSlowModalVisible(false)}
+        item={{ ...userData, country_id: regionData?.id }}
+        updateSlow={(id, v, s11, s31, s101) => handleUpdateSlow(id, v, s11, s31, s101)}
+      />
     </View>
   );
 };

+ 3 - 2
src/screens/InAppScreens/ProfileScreen/Profile/edit-personal-info.tsx

@@ -1,5 +1,5 @@
 import React, { useState } from 'react';
-import { ScrollView, View, Text } from 'react-native';
+import { ScrollView, View } from 'react-native';
 import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
 import { Formik } from 'formik';
 import * as yup from 'yup';
@@ -63,6 +63,7 @@ const ProfileSchema = yup.object({
 
 export const EditPersonalInfo = () => {
   const token = storage.get('token', StoreType.STRING) as string;
+  const uid = storage.get('uid', StoreType.STRING) as string;
   const { mutate: deleteUser } = useDeleteUserMutation();
 
   const { mutate: updateProfile, data: updateResponse, reset } = usePostSetProfileMutation();
@@ -180,7 +181,7 @@ export const EditPersonalInfo = () => {
                     refetchType: 'all'
                   });
                   queryClient.invalidateQueries({
-                    queryKey: userQueryKeys.getProfileInfo(),
+                    queryKey: userQueryKeys.getProfileInfoData(+uid),
                     refetchType: 'all'
                   });
 

+ 0 - 1
src/screens/InAppScreens/ProfileScreen/index.tsx

@@ -75,7 +75,6 @@ const ProfileScreen: FC<Props> = ({ navigation, route }) => {
         navigation.getParent()?.setOptions({
           tabBarStyle: {
             display: 'flex',
-            position: 'absolute',
             ...Platform.select({
               android: {
                 height: 58

+ 25 - 3
src/screens/InAppScreens/TravelsScreen/Components/CountryItem/index.tsx

@@ -10,24 +10,46 @@ import CheckSvg from 'assets/icons/travels-screens/circle-check.svg';
 import CheckRegularSvg from 'assets/icons/travels-screens/circle-check-regular.svg';
 import EditSvg from 'assets/icons/travels-screens/pen-to-square.svg';
 import MarkIcon from 'assets/icons/mark.svg';
+import { NAVIGATION_PAGES } from 'src/types';
 
 const CountryItem = React.memo(
   ({
     item,
     updateSlow,
     openEditModal,
-    token
+    token,
+    setUserData,
+    navigation
   }: {
     item: SlowData;
     updateSlow: (id: number, v: boolean, s11: boolean, s31: boolean, s101: boolean) => void;
     openEditModal: (item: SlowData) => void;
     token: string;
+    setUserData: (data: any) => void;
+    navigation: any;
   }) => {
     const renderDurationIcon = (condition: 0 | 1) =>
       condition && token ? <CheckSvg fill={Colors.DARK_BLUE} /> : <CheckRegularSvg />;
 
     return (
-      <View style={styles.countryItem}>
+      <TouchableOpacity
+        style={styles.countryItem}
+        onPress={() => {
+          setUserData({
+            type: 'countries',
+            visited: Boolean(item.visited),
+            slow11: item.slow11,
+            slow31: item.slow31,
+            slow101: item.slow101
+          });
+          navigation.navigate(NAVIGATION_PAGES.COUNTRY_PREVIEW, {
+            regionId: item.country_id,
+            type: 'country',
+            disabled: token ? false : true,
+            isTravelsScreen: true
+          });
+        }}
+      >
         <View style={styles.headerContainer}>
           <Image source={{ uri: API_HOST + item.flag }} style={styles.flag} />
           <Text style={styles.countryName}>{item.country}</Text>
@@ -87,7 +109,7 @@ const CountryItem = React.memo(
             </View>
           )}
         </View>
-      </View>
+      </TouchableOpacity>
     );
   }
 );

+ 29 - 51
src/screens/InAppScreens/TravelsScreen/CountriesScreen/index.tsx

@@ -1,5 +1,5 @@
 import React, { useCallback, useEffect, useState } from 'react';
-import { View, Text, TouchableOpacity, ScrollView, FlatList } from 'react-native';
+import { View, Text, TouchableOpacity, ScrollView, FlatList, Platform } from 'react-native';
 import ReactModal from 'react-native-modal';
 import * as Progress from 'react-native-progress';
 import { useFocusEffect, useNavigation } from '@react-navigation/native';
@@ -19,12 +19,11 @@ import IngoSvg from 'assets/icons/travels-screens/info.svg';
 import SearchIcon from '../../../../../assets/icons/search.svg';
 import InfoIcon from 'assets/icons/info-solid.svg';
 import { NAVIGATION_PAGES } from 'src/types';
+import { useRegion } from 'src/contexts/RegionContext';
 
 const CountriesScreen = () => {
   const token = storage.get('token', StoreType.STRING) as string;
-  const { data } = useGetSlowQuery(String(token));
-  const { mutate: updateSlow } = usePostSetSlowMutation();
-  const [slow, setSlow] = useState<SlowData[] | null>(null);
+  const { data, refetch } = useGetSlowQuery(String(token));
   const [megaSelectorVisible, setMegaSelectorVisible] = useState(false);
   const [selectedMega, setSelectedMega] = useState<{ name: string; id: number }>({
     name: 'ALL',
@@ -37,15 +36,31 @@ const CountriesScreen = () => {
   const [search, setSearch] = useState<string>('');
   const [filteredSlow, setFilteredSlow] = useState<SlowData[] | null>(null);
   const navigation = useNavigation();
+  const { handleUpdateSlowList: handleUpdateSlow, slow, setSlow, setUserData } = useRegion();
 
   const handleOpenEditModal = (item: SlowData) => {
     setCurrentItem(item);
     setIsEditModalVisible(true);
   };
 
+  useFocusEffect(
+    useCallback(() => {
+      navigation.getParent()?.setOptions({
+        tabBarStyle: {
+          display: 'flex',
+          ...Platform.select({
+            android: {
+              height: 58
+            }
+          })
+        }
+      });
+    }, [navigation])
+  );
+
   useEffect(() => {
     if (slow && slow.length) {
-      token && calcTotalCountries();
+      token && calcTotalScore();
       setFilteredSlow(slow);
     }
   }, [slow]);
@@ -54,21 +69,18 @@ const CountriesScreen = () => {
     if (data && data.result === 'OK') {
       setSearch('');
       if (selectedMega.id === -1) {
-        setSlow(data?.slow);
+        refetch()
       } else {
         setSlow(data?.slow?.filter((item) => item.mega.includes(selectedMega.id)));
       }
     }
   }, [selectedMega]);
 
-  useFocusEffect(
-    useCallback(() => {
-      if (data && data.result === 'OK') {
-        setSlow(data?.slow);
-        token && calcTotalScore();
-      }
-    }, [data])
-  );
+  useEffect(() => {
+    if (data && data.result === 'OK') {
+      setSlow(data?.slow);
+    }
+  }, [data]);
 
   const searchFilter = (text: string) => {
     if (text) {
@@ -92,7 +104,7 @@ const CountriesScreen = () => {
     let slow31 = 0;
     let slow101 = 0;
 
-    data?.slow?.forEach((item: SlowData) => {
+    slow?.forEach((item: SlowData) => {
       visited += item.visited;
       slow11 += item.slow11;
       slow31 += item.slow31;
@@ -102,48 +114,14 @@ const CountriesScreen = () => {
     setTotal({ slow: slow11 + slow31 + slow101, visited });
   };
 
-  const calcTotalCountries = () => {
-    const visited = slow?.filter((item) => item.visited === 1).length || 0;
-    setTotal({ ...total, visited });
-  };
-
-  const handleUpdateSlow = useCallback(
-    (id: number, v: boolean, s11: boolean, s31: boolean, s101: boolean) => {
-      const updatedSlow = slow?.map((item) => {
-        if (item.country_id === id) {
-          return {
-            ...item,
-            visited: Number(v) as 0 | 1,
-            slow11: Number(s11) as 0 | 1,
-            slow31: Number(s31) as 0 | 1,
-            slow101: Number(s101) as 0 | 1
-          };
-        }
-
-        return item;
-      });
-
-      const updatedSlowData = {
-        token,
-        id,
-        v,
-        s11,
-        s31,
-        s101
-      };
-
-      updateSlow(updatedSlowData);
-      updatedSlow && setSlow(updatedSlow);
-    },
-    [slow]
-  );
-
   const renderCountryItem = ({ item }: { item: SlowData }) => (
     <CountryItem
       item={item}
       updateSlow={handleUpdateSlow}
       openEditModal={handleOpenEditModal}
       token={token}
+      setUserData={setUserData}
+      navigation={navigation}
     />
   );
 

+ 12 - 3
src/types/api.ts

@@ -18,7 +18,8 @@ export enum API_ROUTE {
   APP = 'app',
   SEARCH = 'search',
   PROFILE = 'profile',
-  FRIENDS = 'friends'
+  FRIENDS = 'friends',
+  COUNTRIES = 'countries'
 }
 
 export enum API_ENDPOINT {
@@ -103,7 +104,11 @@ export enum API_ENDPOINT {
   LOAD_FRIENDS_SETTINGS = 'load-friends-settings-app',
   UPDATE_FRIEND_STATUS = 'update-friend-status',
   HIDE_SHOW_REQUEST = 'hideShowRequest',
-  GET_FRIENDS_NOTIFICATION = 'is-notification-active'
+  GET_FRIENDS_NOTIFICATION = 'is-notification-active',
+  GET_COUNTRY_SCREEN_DATA = 'get-country-screen-data',
+  GET_USERS_FROM_COUNTRY = 'get-users-from-country',
+  GET_USERS_WHO_VISITED_COUNTRY = 'get-users-who-visited-country',
+  GET_COUNTRY_USER_DATA = 'get-user-data-country-app'
 }
 
 export enum API {
@@ -187,7 +192,11 @@ export enum API {
   LOAD_FRIENDS_SETTINGS = `${API_ROUTE.FRIENDS}/${API_ENDPOINT.LOAD_FRIENDS_SETTINGS}`,
   UPDATE_FRIEND_STATUS = `${API_ROUTE.FRIENDS}/${API_ENDPOINT.UPDATE_FRIEND_STATUS}`,
   HIDE_SHOW_REQUEST = `${API_ROUTE.FRIENDS}/${API_ENDPOINT.HIDE_SHOW_REQUEST}`,
-  GET_FRIENDS_NOTIFICATION = `${API_ROUTE.FRIENDS}/${API_ENDPOINT.GET_FRIENDS_NOTIFICATION}`
+  GET_FRIENDS_NOTIFICATION = `${API_ROUTE.FRIENDS}/${API_ENDPOINT.GET_FRIENDS_NOTIFICATION}`,
+  GET_COUNTRY_SCREEN_DATA = `${API_ROUTE.COUNTRIES}/${API_ENDPOINT.GET_COUNTRY_SCREEN_DATA}`,
+  GET_USERS_FROM_COUNTRY = `${API_ROUTE.COUNTRIES}/${API_ENDPOINT.GET_USERS_FROM_COUNTRY}`,
+  GET_USERS_WHO_VISITED_COUNTRY = `${API_ROUTE.COUNTRIES}/${API_ENDPOINT.GET_USERS_WHO_VISITED_COUNTRY}`,
+  GET_COUNTRY_USER_DATA = `${API_ROUTE.COUNTRIES}/${API_ENDPOINT.GET_COUNTRY_USER_DATA}`
 }
 
 export type BaseAxiosError = AxiosError;

+ 1 - 0
src/types/navigation.ts

@@ -55,4 +55,5 @@ export enum NAVIGATION_PAGES {
   SUGGEST_SERIES = 'inAppSuggestSeries',
   FRIENDS_LIST = 'inAppFriendsList',
   MY_FRIENDS = 'inAppMyFriends',
+  COUNTRY_PREVIEW = 'inAppCountryPreview',
 }

+ 37 - 0
src/utils/mapHelpers.ts

@@ -34,6 +34,43 @@ export const calculateMapRegion = (bounds: turf.BBox): any => {
   };
 };
 
+export const calculateMapCountry = (bbox: turf.BBox, center: number[]): any => {
+  const padding = 1;
+  let latitude = center[1];
+  let longitude = center[0];
+  const [minLng, minLat, maxLng, maxLat] = bbox;
+
+  if (
+    minLat === undefined ||
+    minLng === undefined ||
+    maxLat === undefined ||
+    maxLng === undefined
+  ) {
+    throw new Error("Invalid bbox coordinates");
+  }
+
+  let latitudeDelta = Math.abs(maxLat - minLat) + padding;
+  let longitudeDelta = Math.abs(maxLng - minLng) + padding;
+
+  if (latitudeDelta < 0 || latitudeDelta > 180) {
+    latitudeDelta = Math.min(Math.max(latitudeDelta, 0), 180);
+  }
+
+  if (longitudeDelta < 0 || longitudeDelta > 360) {
+    longitudeDelta = Math.min(Math.max(longitudeDelta, 0), 360);
+  }
+
+  const validLatitude = Math.min(Math.max(latitude, -90), 90);
+  const validLongitude = Math.min(Math.max(longitude, -180), 180);
+
+  return {
+    latitude: validLatitude,
+    longitude: validLongitude,
+    latitudeDelta,
+    longitudeDelta,
+  };
+};
+
 const isBBoxOverlap = (bbox1: number[], bbox2: number[]) => {
   return bbox1[0] <= bbox2[2] &&
          bbox1[2] >= bbox2[0] &&