Viktoriia 9 mesiacov pred
rodič
commit
4d0bbb9881
97 zmenil súbory, kde vykonal 3081 pridanie a 439 odobranie
  1. 23 3
      App.tsx
  2. 33 30
      Route.tsx
  3. 13 4
      app.config.ts
  4. 10 0
      assets/icons/travels-screens/map-location.svg
  5. 1 0
      assets/icons/travels-section/fixers.svg
  6. 9 0
      assets/icons/travels-section/unp.svg
  7. 4 2
      metro.config.js
  8. 2 1
      package.json
  9. 19 7
      src/components/ErrorModal/index.tsx
  10. 3 1
      src/components/FlatList/index.tsx
  11. 11 2
      src/components/FlatList/item.tsx
  12. 4 2
      src/components/HorizontalTabView/index.tsx
  13. 1 1
      src/components/Input/index.tsx
  14. 9 5
      src/contexts/ErrorContext.tsx
  15. 5 2
      src/contexts/NotificationContext.tsx
  16. 1 1
      src/database/geojsonService/index.ts
  17. 4 4
      src/database/index.ts
  18. 17 14
      src/database/seriesRankingService/index.ts
  19. 1 2
      src/database/tilesService/index.ts
  20. 12 8
      src/database/triumphsService/index.ts
  21. 4 4
      src/database/unMastersService/index.ts
  22. 3 3
      src/db/index.ts
  23. 4 1
      src/modules/api/app/queries/use-post-last-dare-db-update.tsx
  24. 4 1
      src/modules/api/app/queries/use-post-last-regions-db-update.tsx
  25. 4 1
      src/modules/api/avatars/queries/use-post-get-avatars.tsx
  26. 80 0
      src/modules/api/fixers/fixers-api.ts
  27. 8 0
      src/modules/api/fixers/fixers-query-keys.tsx
  28. 3 0
      src/modules/api/fixers/index.ts
  29. 6 0
      src/modules/api/fixers/queries/index.ts
  30. 17 0
      src/modules/api/fixers/queries/use-post-add-fixer.tsx
  31. 17 0
      src/modules/api/fixers/queries/use-post-edit-fixer.tsx
  32. 17 0
      src/modules/api/fixers/queries/use-post-get-all-countries.tsx
  33. 17 0
      src/modules/api/fixers/queries/use-post-get-countries.tsx
  34. 17 0
      src/modules/api/fixers/queries/use-post-get-for-country.tsx
  35. 17 0
      src/modules/api/fixers/queries/use-post-save-rating-app.tsx
  36. 4 1
      src/modules/api/friends/queries/use-post-is-notification-active.tsx
  37. 5 2
      src/modules/api/ranking/queries/use-post-get-in-history.tsx
  38. 4 1
      src/modules/api/ranking/queries/use-post-get-in-memoriam.tsx
  39. 4 1
      src/modules/api/ranking/queries/use-post-get-limited-ranking.tsx
  40. 5 2
      src/modules/api/ranking/queries/use-post-get-lpi.tsx
  41. 8 2
      src/modules/api/ranking/queries/use-post-get-un-masters.tsx
  42. 1 0
      src/modules/api/response-type.ts
  43. 4 1
      src/modules/api/series/queries/use-post-get-items-for-series.tsx
  44. 8 2
      src/modules/api/statistics/queries/use-post-get-statistics.tsx
  45. 1 0
      src/modules/api/user/queries/index.ts
  46. 2 2
      src/modules/api/user/queries/use-post-get-map-years.tsx
  47. 2 2
      src/modules/api/user/queries/use-post-get-profile-regions.tsx
  48. 5 1
      src/modules/api/user/queries/use-post-get-profile-updates.tsx
  49. 22 0
      src/modules/api/user/queries/use-post-get-update.tsx
  50. 68 14
      src/modules/api/user/user-api.tsx
  51. 2 1
      src/modules/api/user/user-query-keys.tsx
  52. 2 2
      src/screens/InAppScreens/MapScreen/ClusterItem/index.tsx
  53. 35 1
      src/screens/InAppScreens/MapScreen/CountryViewScreen/index.tsx
  54. 3 2
      src/screens/InAppScreens/MapScreen/FilterModal/index.tsx
  55. 1 1
      src/screens/InAppScreens/MapScreen/MarkerItem/index.tsx
  56. 101 1
      src/screens/InAppScreens/MapScreen/RegionViewScreen/index.tsx
  57. 23 3
      src/screens/InAppScreens/MapScreen/RegionViewScreen/styles.tsx
  58. 3 3
      src/screens/InAppScreens/MapScreen/UsersListScreen/index.tsx
  59. 175 87
      src/screens/InAppScreens/MapScreen/index.tsx
  60. 111 88
      src/screens/InAppScreens/ProfileScreen/Components/PersonalInfo.tsx
  61. 139 32
      src/screens/InAppScreens/ProfileScreen/RegionsRenderer/index.tsx
  62. 214 0
      src/screens/InAppScreens/ProfileScreen/UpdatesRenderer/index.tsx
  63. 46 0
      src/screens/InAppScreens/ProfileScreen/UpdatesRenderer/styles.tsx
  64. 2 3
      src/screens/InAppScreens/ProfileScreen/index.tsx
  65. 3 3
      src/screens/InAppScreens/TravellersScreen/SeriesRankingListScreen/index.tsx
  66. 338 0
      src/screens/InAppScreens/TravelsScreen/AddNewFixerScreen/index.tsx
  67. 91 0
      src/screens/InAppScreens/TravelsScreen/AddNewFixerScreen/styles.tsx
  68. 6 2
      src/screens/InAppScreens/TravelsScreen/AddPhotoScreen/index.tsx
  69. 1 1
      src/screens/InAppScreens/TravelsScreen/AddRegionsScreen/index.tsx
  70. 1 0
      src/screens/InAppScreens/TravelsScreen/Components/CountryItem/index.tsx
  71. 225 0
      src/screens/InAppScreens/TravelsScreen/Components/FixerItem/index.tsx
  72. 86 0
      src/screens/InAppScreens/TravelsScreen/Components/FixerItem/styles.tsx
  73. 151 0
      src/screens/InAppScreens/TravelsScreen/Components/RateModal/index.tsx
  74. 48 0
      src/screens/InAppScreens/TravelsScreen/Components/RateModal/styles.tsx
  75. 29 0
      src/screens/InAppScreens/TravelsScreen/Components/Star/index.tsx
  76. 71 0
      src/screens/InAppScreens/TravelsScreen/Components/StarRating/index.tsx
  77. 2 0
      src/screens/InAppScreens/TravelsScreen/Components/index.ts
  78. 23 5
      src/screens/InAppScreens/TravelsScreen/CountriesScreen/index.tsx
  79. 50 15
      src/screens/InAppScreens/TravelsScreen/DareScreen/index.tsx
  80. 55 0
      src/screens/InAppScreens/TravelsScreen/FixersCommentsScreen/index.tsx
  81. 29 0
      src/screens/InAppScreens/TravelsScreen/FixersCommentsScreen/styles.tsx
  82. 174 0
      src/screens/InAppScreens/TravelsScreen/FixersScreen/index.tsx
  83. 39 0
      src/screens/InAppScreens/TravelsScreen/FixersScreen/styles.tsx
  84. 80 38
      src/screens/InAppScreens/TravelsScreen/RegionsScreen/index.tsx
  85. 1 1
      src/screens/InAppScreens/TravelsScreen/RegionsScreen/styles.tsx
  86. 4 4
      src/screens/InAppScreens/TravelsScreen/SeriesItemScreen/index.tsx
  87. 9 2
      src/screens/InAppScreens/TravelsScreen/index.tsx
  88. 15 0
      src/screens/InAppScreens/TravelsScreen/utils/constants.ts
  89. 35 0
      src/screens/InAppScreens/TravelsScreen/utils/types.ts
  90. 37 0
      src/screens/InfoScreens/FixersInfoScreen/index.tsx
  91. 16 0
      src/screens/InfoScreens/FixersInfoScreen/styles.tsx
  92. 1 0
      src/screens/InfoScreens/index.ts
  93. 16 2
      src/screens/RegisterScreen/EditAccount/index.tsx
  94. 19 4
      src/types/api.ts
  95. 4 0
      src/types/navigation.ts
  96. 1 1
      src/utils/mapHelpers.ts
  97. 21 7
      src/utils/request.ts

+ 23 - 3
App.tsx

@@ -1,7 +1,9 @@
 import 'react-native-gesture-handler';
+import 'expo-splash-screen';
 import { QueryClientProvider } from '@tanstack/react-query';
 import { NavigationContainer } from '@react-navigation/native';
 import { queryClient } from 'src/utils/queryClient';
+import * as Sentry from '@sentry/react-native';
 
 import Route from './Route';
 import { ConnectionProvider } from 'src/contexts/ConnectionContext';
@@ -12,6 +14,18 @@ import { useEffect } from 'react';
 import { setupInterceptors } from 'src/utils/request';
 import { ErrorModal } from 'src/components';
 import { NotificationProvider } from 'src/contexts/NotificationContext';
+import React from 'react';
+
+const routingInstrumentation = new Sentry.ReactNavigationInstrumentation({
+  enableTimeToInitialDisplay: true
+});
+
+Sentry.init({
+  dsn: 'https://c9b37005f4be22a17a582603ebc17598@o4507781200543744.ingest.de.sentry.io/4507781253824592',
+  integrations: [new Sentry.ReactNativeTracing({ routingInstrumentation })],
+  debug: false,
+  ignoreErrors: ['Network Error', 'ECONNABORTED', 'timeout of 10000ms exceeded'],
+});
 
 const App = () => {
   return (
@@ -27,6 +41,7 @@ const App = () => {
 
 const InnerApp = () => {
   const errorContext = useError();
+  const navigation = React.useRef(null);
 
   useEffect(() => {
     setupInterceptors(errorContext);
@@ -35,9 +50,14 @@ const InnerApp = () => {
   return (
     <ConnectionProvider>
       <RegionProvider>
-        <NavigationContainer>
-          <ConnectionBanner />
+        <NavigationContainer
+          ref={navigation}
+          onReady={() => {
+            routingInstrumentation.registerNavigationContainer(navigation);
+          }}
+        >
           <Route />
+          <ConnectionBanner />
           <ErrorModal />
         </NavigationContainer>
       </RegionProvider>
@@ -45,4 +65,4 @@ const InnerApp = () => {
   );
 };
 
-export default App;
+export default Sentry.wrap(App);

+ 33 - 30
Route.tsx

@@ -1,7 +1,8 @@
 import React, { useEffect, useState } from 'react';
 import { useFonts } from 'expo-font';
+import { enableScreens } from 'react-native-screens';
 import * as SplashScreen from 'expo-splash-screen';
-import { AppState, Platform } from 'react-native';
+import { Platform } from 'react-native';
 import * as Notifications from 'expo-notifications';
 
 import { createStackNavigator, TransitionPresets } from '@react-navigation/stack';
@@ -47,6 +48,9 @@ import AddRegionsScreen from 'src/screens/InAppScreens/TravelsScreen/AddRegionsS
 import CountriesScreen from 'src/screens/InAppScreens/TravelsScreen/CountriesScreen';
 import RegionsScreen from 'src/screens/InAppScreens/TravelsScreen/RegionsScreen';
 import DareScreen from 'src/screens/InAppScreens/TravelsScreen/DareScreen';
+import FixersScreen from 'src/screens/InAppScreens/TravelsScreen/FixersScreen';
+import AddNewFixerScreen from 'src/screens/InAppScreens/TravelsScreen/AddNewFixerScreen';
+import FixersCommentsScreen from 'src/screens/InAppScreens/TravelsScreen/FixersCommentsScreen';
 
 import { API, NAVIGATION_PAGES } from './src/types';
 import { storage, StoreType } from './src/storage';
@@ -68,10 +72,10 @@ import {
   RegionsInfoScreen,
   EarthInfoScreen,
   DareInfoScreen,
-  TripsInfoScreen
+  TripsInfoScreen,
+  FixersInfoScreen
 } from 'src/screens/InfoScreens';
 import RegionViewScreen from 'src/screens/InAppScreens/MapScreen/RegionViewScreen';
-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';
@@ -82,12 +86,12 @@ import { useNotification } from 'src/contexts/NotificationContext';
 
 enableScreens();
 
+SplashScreen.preventAutoHideAsync();
+
 const ScreenStack = createStackNavigator();
 const BottomTab = createBottomTabNavigator();
 const MapDrawer = createDrawerNavigator();
 
-SplashScreen.preventAutoHideAsync();
-
 const Route = () => {
   const [token, setToken] = useState<string | null>(
     storage.get('token', StoreType.STRING) as string
@@ -103,24 +107,8 @@ const Route = () => {
   });
   const [dbLoaded, setDbLoaded] = useState(false);
   const uid = storage.get('uid', StoreType.STRING);
-  const [appState, setAppState] = useState<string>(AppState.currentState);
   const { updateNotificationStatus } = useNotification();
 
-  useEffect(() => {
-    const handleAppStateChange = async (nextAppState: string) => {
-      if (appState.match(/inactive|background/) && nextAppState === 'active') {
-        await checkNmToken();
-      }
-      setAppState(nextAppState);
-    };
-
-    const subscription = AppState.addEventListener('change', handleAppStateChange);
-
-    return () => {
-      subscription.remove();
-    };
-  }, [appState]);
-
   const checkNmToken = async () => {
     if (token && uid) {
       try {
@@ -203,7 +191,7 @@ const Route = () => {
     }
   };
 
-  if (!fontsLoaded) {
+  if (!fontsLoaded || !dbLoaded) {
     return null;
   }
 
@@ -275,18 +263,14 @@ const Route = () => {
               component={CountryViewScreen}
               options={regionViewScreenOptions}
             />
-
+            <ScreenStack.Screen name={NAVIGATION_PAGES.ADD_PHOTO} component={AddPhotoScreen} />
             <ScreenStack.Screen name={NAVIGATION_PAGES.PROFILE_TAB} component={ProfileScreen} />
             <ScreenStack.Screen
               name={NAVIGATION_PAGES.EDIT_PERSONAL_INFO}
               component={EditPersonalInfo}
             />
             <ScreenStack.Screen name={NAVIGATION_PAGES.SETTINGS} component={Settings} />
-            <ScreenStack.Screen
-              name={NAVIGATION_PAGES.MY_FRIENDS}
-              component={MyFriendsScreen}
-              options={regionViewScreenOptions}
-            />
+            <ScreenStack.Screen name={NAVIGATION_PAGES.MY_FRIENDS} component={MyFriendsScreen} />
           </ScreenStack.Navigator>
         )}
       </BottomTab.Screen>
@@ -306,6 +290,12 @@ const Route = () => {
             <ScreenStack.Screen name={NAVIGATION_PAGES.COUNTRIES} component={CountriesScreen} />
             <ScreenStack.Screen name={NAVIGATION_PAGES.REGIONS} component={RegionsScreen} />
             <ScreenStack.Screen name={NAVIGATION_PAGES.DARE} component={DareScreen} />
+            <ScreenStack.Screen name={NAVIGATION_PAGES.FIXERS} component={FixersScreen} />
+            <ScreenStack.Screen name={NAVIGATION_PAGES.ADD_FIXER} component={AddNewFixerScreen} />
+            <ScreenStack.Screen
+              name={NAVIGATION_PAGES.FIXERS_COMMENTS}
+              component={FixersCommentsScreen}
+            />
             <ScreenStack.Screen
               name={NAVIGATION_PAGES.REGION_PREVIEW}
               component={RegionViewScreen}
@@ -385,11 +375,23 @@ const Route = () => {
               component={UsersListScreen}
               options={regionViewScreenOptions}
             />
+            <ScreenStack.Screen name={NAVIGATION_PAGES.MY_FRIENDS} component={MyFriendsScreen} />
+            <ScreenStack.Screen
+              name={NAVIGATION_PAGES.REGION_PREVIEW}
+              component={RegionViewScreen}
+              options={regionViewScreenOptions}
+            />
+            <ScreenStack.Screen
+              name={NAVIGATION_PAGES.USERS_LIST}
+              component={UsersListScreen}
+              options={regionViewScreenOptions}
+            />
             <ScreenStack.Screen
-              name={NAVIGATION_PAGES.MY_FRIENDS}
-              component={MyFriendsScreen}
+              name={NAVIGATION_PAGES.COUNTRY_PREVIEW}
+              component={CountryViewScreen}
               options={regionViewScreenOptions}
             />
+            <ScreenStack.Screen name={NAVIGATION_PAGES.ADD_PHOTO} component={AddPhotoScreen} />
           </ScreenStack.Navigator>
         )}
       </BottomTab.Screen>
@@ -427,6 +429,7 @@ const Route = () => {
       <ScreenStack.Screen name={NAVIGATION_PAGES.DARE_INFO} component={DareInfoScreen} />
       <ScreenStack.Screen name={NAVIGATION_PAGES.REGIONS_INFO} component={RegionsInfoScreen} />
       <ScreenStack.Screen name={NAVIGATION_PAGES.TRIPS_INFO} component={TripsInfoScreen} />
+      <ScreenStack.Screen name={NAVIGATION_PAGES.FIXERS_INFO} component={FixersInfoScreen} />
       <ScreenStack.Screen name={NAVIGATION_PAGES.EARTH_INFO} component={EarthInfoScreen} />
       <ScreenStack.Screen name={NAVIGATION_PAGES.IN_APP}>
         {() => (

+ 13 - 4
app.config.ts

@@ -20,7 +20,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
   owner: 'nomadmaniaou',
   scheme: 'nm',
   // Should be updated after every production release (deploy to AppStore/PlayMarket)
-  version: '2.0.9',
+  version: '2.0.16',
   // Should be updated after every dependency change
   runtimeVersion: '1.5',
   orientation: 'portrait',
@@ -40,7 +40,8 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
   },
   splash: {
     image: './assets/loading-screen.png',
-    resizeMode: 'cover'
+    resizeMode: 'cover',
+    backgroundColor: '#ffffff'
   },
   notification: {
     icon: './assets/notification-icon.png'
@@ -87,7 +88,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
       'INTERNET',
       'CAMERA'
     ],
-    versionCode: 60 // next version submitted to Google Play needs to be higher than that 2.0.9
+    versionCode: 68 // next version submitted to Google Play needs to be higher than that 2.0.16
   },
   plugins: [
     [
@@ -102,10 +103,18 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
       {
         android: {
           minSdkVersion: 24,
-          targetSdkVersion: 33,
+          targetSdkVersion: 34,
           // kotlinVersion: '1.7.1'
         }
       }
     ],
+    [
+      "@sentry/react-native/expo",
+      {
+        organization: env.SENTRY_ORG,
+        project: env.SENTRY_PROJECT,
+        url: "https://sentry.io/"
+      }
+    ]
   ]
 });

+ 10 - 0
assets/icons/travels-screens/map-location.svg

@@ -0,0 +1,10 @@
+<svg width="20" height="18" viewBox="0 0 20 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_3810_38142)">
+<path d="M14.1667 4.16667C14.1667 6.0625 11.6285 9.44097 10.5139 10.8333C10.2465 11.1667 9.75 11.1667 9.48611 10.8333C8.37153 9.44097 5.83333 6.0625 5.83333 4.16667C5.83333 1.86458 7.69792 0 10 0C12.3021 0 14.1667 1.86458 14.1667 4.16667ZM14.4444 6.95833C14.566 6.71875 14.6771 6.47917 14.7778 6.24306C14.7951 6.20139 14.8125 6.15625 14.8299 6.11458L18.8576 4.50347C19.4062 4.28472 20 4.6875 20 5.27778V14.6806C20 15.0208 19.7917 15.3264 19.4757 15.4549L14.4444 17.4653V6.95833ZM4.77778 4.80208C4.86111 5.29167 5.02778 5.78472 5.22222 6.24306C5.32292 6.47917 5.43403 6.71875 5.55556 6.95833V15.6875L1.14236 17.4549C0.59375 17.6736 0 17.2708 0 16.6806V7.27778C0 6.9375 0.208333 6.63194 0.524306 6.50347L4.78125 4.80208H4.77778ZM11.3819 11.5278C11.8646 10.9236 12.6215 9.94097 13.3333 8.85417V17.5104L6.66667 15.6042V8.85417C7.37847 9.94097 8.13542 10.9236 8.61806 11.5278C9.32986 12.4167 10.6701 12.4167 11.3819 11.5278ZM10 5.27778C10.3684 5.27778 10.7216 5.13145 10.9821 4.87098C11.2426 4.61051 11.3889 4.25725 11.3889 3.88889C11.3889 3.52053 11.2426 3.16726 10.9821 2.9068C10.7216 2.64633 10.3684 2.5 10 2.5C9.63164 2.5 9.27837 2.64633 9.01791 2.9068C8.75744 3.16726 8.61111 3.52053 8.61111 3.88889C8.61111 4.25725 8.75744 4.61051 9.01791 4.87098C9.27837 5.13145 9.63164 5.27778 10 5.27778Z" fill="white"/>
+</g>
+<defs>
+<clipPath id="clip0_3810_38142">
+<rect width="20" height="17.7778" fill="white"/>
+</clipPath>
+</defs>
+</svg>

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 1 - 0
assets/icons/travels-section/fixers.svg


+ 9 - 0
assets/icons/travels-section/unp.svg

@@ -0,0 +1,9 @@
+<svg width="18" height="20" viewBox="0 0 18 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<mask id="mask0_3928_35979" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="18" height="20">
+<path d="M17.6177 0H0V19.753H17.6177V0Z" fill="white"/>
+</mask>
+<g mask="url(#mask0_3928_35979)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M0 18.686V3.73766C0 3.14705 0.477132 2.66992 1.06774 2.66992C1.65832 2.66992 2.13548 3.14705 2.13548 3.73766L2.13535 4.27105L4.43766 3.69092C5.7089 3.37394 7.05354 3.52075 8.22468 4.10799C9.76952 4.88208 11.588 4.88208 13.1328 4.10799L13.4531 3.94783C14.1404 3.60417 14.9479 4.10465 14.9479 4.87207V13.2168C14.9479 13.6639 14.671 14.061 14.2539 14.2178L13.0961 14.6516C11.5546 15.2321 9.84292 15.142 8.36815 14.4047C7.10359 13.774 5.65217 13.6139 4.28084 13.9576L2.13548 14.4946V18.686C2.13548 19.2765 1.65832 19.7536 1.06774 19.7536C0.477132 19.7536 0 19.2765 0 18.686ZM2.13545 12.8427L3.89046 12.4027C5.63549 11.9689 7.47395 12.1691 9.08218 12.9733C10.1566 13.5105 11.4078 13.5738 12.5322 13.1534L13.3463 12.8498V5.76961C11.4678 6.53369 9.3391 6.45694 7.50731 5.54272C6.67651 5.12898 5.72558 5.02555 4.8247 5.2491L2.13538 5.92282L2.13545 12.8427Z" fill="#0F3F4F"/>
+<path d="M3.73733 0C3.14671 0 2.66968 0.477036 2.66968 1.06765V2.56197C2.75165 2.71407 2.80301 2.88493 2.80301 3.06962V3.60394L5.10523 3.02617C6.37634 2.70913 7.72152 2.85629 8.89188 3.44295C10.4376 4.21727 12.2558 4.21727 13.8005 3.44295L14.1205 3.28295C14.8079 2.93925 15.6158 3.43999 15.6158 4.2074V11.9545C15.6662 11.9368 15.7166 11.921 15.7659 11.9022L16.9235 11.4686C17.3412 11.3116 17.6178 10.9145 17.6178 10.4671V2.20543C17.6178 1.43802 16.8099 0.937282 16.1225 1.28098L15.8025 1.44098C14.2578 2.2153 12.4395 2.2153 10.8948 1.44098C9.72349 0.854319 8.37831 0.707159 7.1072 1.02419L4.80498 1.60197V1.06765C4.80498 0.477036 4.32795 0 3.73733 0ZM4.80498 13.2454L2.80301 13.7471V16.5234C2.98375 16.8563 3.3314 17.0834 3.73733 17.0834C4.32795 17.0834 4.80498 16.6064 4.80498 16.0158V13.2454Z" fill="#0F3F4F"/>
+</g>
+</svg>

+ 4 - 2
metro.config.js

@@ -1,7 +1,9 @@
-const { getDefaultConfig } = require('expo/metro-config');
+// const { getDefaultConfig } = require('expo/metro-config');
+const { getSentryExpoConfig } = require('@sentry/react-native/metro');
 
 module.exports = (() => {
-  const config = getDefaultConfig(__dirname);
+  // const config = getDefaultConfig(__dirname);
+  const config = getSentryExpoConfig(__dirname);
 
   const { transformer, resolver } = config;
 

+ 2 - 1
package.json

@@ -13,7 +13,6 @@
   },
   "dependencies": {
     "@react-native-community/datetimepicker": "7.2.0",
-    "@react-native-community/masked-view": "^0.1.11",
     "@react-native-community/netinfo": "9.3.10",
     "@react-navigation/bottom-tabs": "^6.5.11",
     "@react-navigation/drawer": "^6.6.15",
@@ -21,6 +20,7 @@
     "@react-navigation/native": "^6.1.9",
     "@react-navigation/native-stack": "^6.9.17",
     "@react-navigation/stack": "^6.3.20",
+    "@sentry/react-native": "^5.29.0",
     "@shopify/flash-list": "1.4.3",
     "@tanstack/react-query": "latest",
     "@turf/turf": "^6.5.0",
@@ -45,6 +45,7 @@
     "formik": "^2.4.5",
     "moment": "^2.29.4",
     "patch-package": "^8.0.0",
+    "promise": "^8.3.0",
     "react": "18.2.0",
     "react-native": "0.72.10",
     "react-native-animated-pagination-dot": "^0.4.0",

+ 19 - 7
src/components/ErrorModal/index.tsx

@@ -11,18 +11,30 @@ import { ButtonVariants } from 'src/types/components';
 import { Button } from '../Button';
 import { CommonActions, useNavigation } from '@react-navigation/native';
 import { NAVIGATION_PAGES } from 'src/types';
+import { storage } from 'src/storage';
+import { useNotification } from 'src/contexts/NotificationContext';
 
 export const ErrorModal = () => {
-  const { error, hideError } = useError();
+  const { error, hideError, navigateToLogin } = useError();
   const navigation = useNavigation();
+  const { updateNotificationStatus } = useNotification();
 
   const handleClose = () => {
-    navigation.dispatch(
-      CommonActions.reset({
-        index: 1,
-        routes: [{ name: NAVIGATION_PAGES.IN_APP_MAP_TAB }]
-      })
-    );
+    if (navigateToLogin) {
+      storage.remove('token');
+      storage.remove('uid');
+      storage.remove('currentUserData');
+      storage.remove('visitedTilesUrl');
+      storage.remove('filterSettings');
+      updateNotificationStatus();
+
+      navigation.dispatch(
+        CommonActions.reset({
+          index: 1,
+          routes: [{ name: NAVIGATION_PAGES.WELCOME }]
+        })
+      );
+    }
     hideError();
   };
 

+ 3 - 1
src/components/FlatList/index.tsx

@@ -12,11 +12,12 @@ type Props = {
   itemObject: (object: any) => void;
   initialData?: ItemData[] | string[];
   date?: boolean;
+  countries?: boolean;
 };
 
 //TODO: rework to generic types + custom props
 
-export const FlatList: FC<Props> = ({ itemObject, initialData, date }) => {
+export const FlatList: FC<Props> = ({ itemObject, initialData, date, countries }) => {
   const [selectedObject, setSelectedObject] = useState<{ name: string; id: number } | string>();
   const [search, setSearch] = useState('');
   const [filteredData, setFilteredData] = useState<ItemData[] | string[]>([]);
@@ -73,6 +74,7 @@ export const FlatList: FC<Props> = ({ itemObject, initialData, date }) => {
         backgroundColor={backgroundColor}
         initial={initialData ? true : false}
         date={date}
+        countries={countries}
       />
     );
   };

+ 11 - 2
src/components/FlatList/item.tsx

@@ -6,7 +6,15 @@ import { styles } from './styles';
 import MarkSVG from '../../../assets/icons/mark.svg';
 import { API_HOST } from '../../constants';
 
-export const Item = ({ item, onPress, backgroundColor, selected, initial, date }: ItemProps) => {
+export const Item = ({
+  item,
+  onPress,
+  backgroundColor,
+  selected,
+  initial,
+  date,
+  countries
+}: ItemProps) => {
   const name = initial && date ? item : initial ? item.country : item.name?.split('–') || '';
 
   return (
@@ -28,7 +36,7 @@ export const Item = ({ item, onPress, backgroundColor, selected, initial, date }
             <Text style={[styles.title, { color: Colors.DARK_BLUE, flexShrink: 1 }]}>
               {initial ? (name as string) : (name as string[])[0]}
             </Text>
-            {initial && !date && item.country !== 'All Regions' && (
+            {initial && !date && item.country !== 'All Regions' && !countries && (
               <View style={styles.regionIndicator}>
                 <Text style={[styles.text, { color: Colors.WHITE, fontWeight: 'bold' }]}>
                   {item.dare ? 'DARE' : 'NM'}
@@ -63,4 +71,5 @@ type ItemProps = {
   selected: boolean;
   initial?: boolean;
   date?: boolean;
+  countries?: boolean;
 };

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

@@ -16,7 +16,8 @@ export const HorizontalTabView = ({
   withMark,
   onDoubleClick,
   lazy = false,
-  withNotification = false
+  withNotification = false,
+  maxTabHeight
 }: {
   index: number;
   setIndex: React.Dispatch<React.SetStateAction<number>>;
@@ -26,6 +27,7 @@ export const HorizontalTabView = ({
   onDoubleClick?: () => void;
   lazy?: boolean;
   withNotification?: boolean;
+  maxTabHeight?: number;
 }) => {
   const renderTabBar = (props: any) => (
     <TabBar
@@ -51,7 +53,7 @@ export const HorizontalTabView = ({
       style={styles.tabBar}
       activeColor={Colors.ORANGE}
       inactiveColor={Colors.DARK_BLUE}
-      tabStyle={styles.tabStyle}
+      tabStyle={[styles.tabStyle, maxTabHeight ? { maxHeight: maxTabHeight } : {}]}
       pressColor={'transparent'}
     />
   );

+ 1 - 1
src/components/Input/index.tsx

@@ -60,7 +60,7 @@ export const Input: FC<Props> = ({
         style={[
           [styles.wrapper, formikError ? styles.inputError : null, { backgroundColor }],
           { flexDirection: 'row', alignItems: 'center' },
-          multiline ? { height: height ?? 100 } : { height: height ?? 44 }
+          multiline ? { minHeight: height ?? 100, height: 'auto' } : { height: height ?? 44 }
         ]}
       >
         {icon ? (

+ 9 - 5
src/contexts/ErrorContext.tsx

@@ -2,19 +2,23 @@ import React, { createContext, useState, useContext } from 'react';
 
 const ErrorContext = createContext<{
   error: string | null;
-  showError: (message: string) => void;
+  showError: (message: string, loginNeeded: boolean) => void;
   hideError: () => void;
+  navigateToLogin: boolean;
 }>({
   error: null,
-  showError: (message: string) => {},
-  hideError: () => {}
+  showError: (message: string, loginNeeded: boolean) => {},
+  hideError: () => {},
+  navigateToLogin: false
 });
 
 export const ErrorProvider = ({ children }: { children: React.ReactNode }) => {
   const [error, setError] = useState<string | null>(null);
+  const [navigateToLogin, setNavigateToLogin] = useState<boolean>(false);
 
-  const showError = (message: string) => {
+  const showError = (message: string, loginNeeded: boolean) => {
     setError(message);
+    setNavigateToLogin(loginNeeded);
   };
 
   const hideError = () => {
@@ -22,7 +26,7 @@ export const ErrorProvider = ({ children }: { children: React.ReactNode }) => {
   };
 
   return (
-    <ErrorContext.Provider value={{ error, showError, hideError }}>
+    <ErrorContext.Provider value={{ error, showError, hideError, navigateToLogin }}>
       {children}
     </ErrorContext.Provider>
   );

+ 5 - 2
src/contexts/NotificationContext.tsx

@@ -19,8 +19,11 @@ export const NotificationProvider = ({ children }: { children: React.ReactNode }
       try {
         const data = await fetchFriendsNotification(token as string);
         const isActive = data && data.active;
-        setIsNotificationActive(isActive as boolean);
-        storage.set('friendsNotification', isActive as boolean);
+        
+        if (typeof isActive === 'boolean') {
+          setIsNotificationActive(isActive);
+          storage.set('friendsNotification', isActive);
+        }
       } catch (error) {
         console.error('Failed to fetch notifications', error);
       }

+ 1 - 1
src/database/geojsonService/index.ts

@@ -45,7 +45,7 @@ export const fetchJsonData = async () => {
   try {
     const response = await axios.get(`${API_HOST}/static/json/mqp.geojson`);
     if (response.status !== 200) {
-      throw new Error('Network response error');
+      console.error('Network response error');
     }
     jsonData = response.data;
     await saveJsonDataToLocal(jsonData);

+ 4 - 4
src/database/index.ts

@@ -71,25 +71,25 @@ const updateMasterRanking = async () => {
   const token = storage.get('token', StoreType.STRING) as string || '';
   const dataLimitedRanking = await fetchLimitedRanking();
 
-  if (dataLimitedRanking && dataLimitedRanking.data) {
+  if (dataLimitedRanking && dataLimitedRanking?.data) {
     storage.set('masterRanking', JSON.stringify(dataLimitedRanking.data));
   }
 
   const dataLpi = await fetchLpi();
 
-  if (dataLpi && dataLpi.data) {
+  if (dataLpi && dataLpi?.data) {
     storage.set('lpiRanking', JSON.stringify(dataLpi.data));
   }
 
   const dataInHistory = await fetchInHistory();
 
-  if (dataInHistory && dataInHistory.data) {
+  if (dataInHistory && dataInHistory?.data) {
     storage.set('inHistoryRanking', JSON.stringify(dataInHistory.data));
   }
 
   const dataInMemoriam = await fetchInMemoriam();
 
-  if (dataInMemoriam && dataInMemoriam.data) {
+  if (dataInMemoriam && dataInMemoriam?.data) {
     storage.set('inMemoriamRanking', JSON.stringify(dataInMemoriam.data));
   }
 

+ 17 - 14
src/database/seriesRankingService/index.ts

@@ -11,20 +11,23 @@ function saveData<T>(key: string, data: T) {
 
 export async function saveSeriesRankingData() {
   const response = await seriesApi.getSeriesGroupsRanking();
-  const groups = response.data.data;
-  saveData('groups', groups);
+  if (response && response.data) {
+    const groups = response?.data?.data;
+    groups && saveData('groups', groups);
+    if (!groups) return;
 
-  await Promise.all(
-    groups.map(async (group) => {
-      const response = await seriesApi.getSeriesRanking(group.id, 0, 50);
-      saveData(`${group.id}`, response.data.data);
+    await Promise.all(
+      groups.map(async (group) => {
+        const res = await seriesApi.getSeriesRanking(group.id, 0, 50);
+        res?.data?.data && saveData(`${group.id}`, res.data.data);
 
-      if (group.series) {
-        group.series.map(async (series) => {
-          const subseries = await seriesApi.getSeriesRanking(series.id, 0, 50);
-          saveData(`${series.id}`, subseries.data.data);
-        });
-      }
-    })
-  );
+        if (group.series) {
+          group.series.map(async (series) => {
+            const subseries = await seriesApi.getSeriesRanking(series.id, 0, 50);
+            saveData(`${series.id}`, subseries.data.data);
+          });
+        }
+      })
+    );
+  }
 }

+ 1 - 2
src/database/tilesService/index.ts

@@ -64,8 +64,7 @@ async function downloadTiles(tileType: TileType): Promise<void> {
 export async function initTilesDownload(userId: string): Promise<void> {
   let tileTypes: TileType[] = [
     {url: '/tiles_osm', type: 'background', maxZoom: 5},
-    {url: '/tiles_nm/grid', type: 'grid', maxZoom: 5},
-    {url: '/tiles_nm/regions_mqp', type: 'regions_mqp', maxZoom: 4},
+    {url: '/tiles_nm/grid', type: 'grid', maxZoom: 4}
   ];
 
   for (const type of tileTypes) {

+ 12 - 8
src/database/triumphsService/index.ts

@@ -11,13 +11,17 @@ function saveData<T>(key: string, data: T) {
 
 export async function saveTriumphsData() {
   const response = await triumphsApi.getDates();
-  const last30Dates = response.data.dates.slice(-30);
-  saveData('dates', last30Dates);
+  if (response && response.data) {
+    const last30Dates = response.data.dates.slice(-30);
+    saveData('dates', last30Dates);
 
-  await Promise.all(
-    last30Dates.map(async (date) => {
-      const response = await triumphsApi.getData(date);
-      saveData(`data_${date}`, response.data.triumphs);
-    })
-  );
+    await Promise.all(
+      last30Dates.map(async (date) => {
+        const res = await triumphsApi.getData(date);
+        if (res && res.data) {
+          saveData(`data_${date}`, res.data.triumphs);
+        }
+      })
+    );
+  }
 }

+ 4 - 4
src/database/unMastersService/index.ts

@@ -98,8 +98,8 @@ export function getMastersByCountryOfOrigin() {
 
     countryBlocks.push({
       country,
-      count: mastersList.length,
-      masters: mastersList
+      count: mastersList?.length ?? 0,
+      masters: mastersList ?? []
     });
   }
 
@@ -123,8 +123,8 @@ export function getMastersByYearOfCompletion() {
 
     yearBlocks.push({
       year,
-      count: mastersList.length,
-      masters: mastersList
+      count: mastersList?.length ?? 0,
+      masters: mastersList ?? []
     });
   }
 

+ 3 - 3
src/db/index.ts

@@ -136,7 +136,7 @@ async function refreshNmDatabase() {
     const nmResponse = await FileSystem.downloadAsync(nmUrl, nmFileUri);
 
     if (nmResponse.status !== 200) {
-      throw new Error(`Failed to download the nmDb file: Status code ${nmResponse.status}`);
+      console.error(`Failed to download the nmDb file: Status code ${nmResponse.status}`);
     }
 
     db1 = null;
@@ -156,7 +156,7 @@ async function refreshDarePlacesDatabase() {
     const dareResponse = await FileSystem.downloadAsync(dareUrl, dareFileUri);
 
     if (dareResponse.status !== 200) {
-      throw new Error(`Failed to download the dareDb file: Status code ${dareResponse.status}`);
+      console.error(`Failed to download the dareDb file: Status code ${dareResponse.status}`);
     }
 
     db2 = null;
@@ -176,7 +176,7 @@ async function refreshCountriesDatabase() {
     const countriesResponse = await FileSystem.downloadAsync(countriesUrl, countriesFileUri);
 
     if (countriesResponse.status !== 200) {
-      throw new Error(
+      console.error(
         `Failed to download the countriesDb file: Status code ${countriesResponse.status}`
       );
     }

+ 4 - 1
src/modules/api/app/queries/use-post-last-dare-db-update.tsx

@@ -6,7 +6,10 @@ export const fetchLastDareDbUpdate = async (date: string) => {
   try {
     const data: PostGetLastUpdate = await queryClient.fetchQuery({
       queryKey: appQueryKeys.getLastDareUpdate(),
-      queryFn: () => appApi.getLastDareUpdate(date).then((res) => res.data),
+      queryFn: async () => {
+        const response = await appApi.getLastDareUpdate(date);
+        return response.data;
+      },
       gcTime: 0,
       staleTime: 0
     });

+ 4 - 1
src/modules/api/app/queries/use-post-last-regions-db-update.tsx

@@ -6,7 +6,10 @@ export const fetchLastRegionsDbUpdate = async (date: string) => {
   try {
     const data: PostGetLastUpdate = await queryClient.fetchQuery({
       queryKey: appQueryKeys.getLastRegionsUpdate(),
-      queryFn: () => appApi.getLastRegionsUpdate(date).then((res) => res.data),
+      queryFn: async () => {
+        const response = await appApi.getLastRegionsUpdate(date);
+        return response.data;
+      },
       gcTime: 0,
       staleTime: 0
     });

+ 4 - 1
src/modules/api/avatars/queries/use-post-get-avatars.tsx

@@ -6,7 +6,10 @@ export const fetchUpdatedAvatars = async (date: string) => {
   try {
     const data: PostGetAvatars = await queryClient.fetchQuery({
       queryKey: avatarsQueryKeys.getUpdatedAvatars(date),
-      queryFn: () => avatarsApi.getUpdatedAvatars(date).then((res) => res.data),
+      queryFn: async () => {
+        const response = await avatarsApi.getUpdatedAvatars(date);
+        return response.data;
+      },
       gcTime: 0,
       staleTime: 0
     });

+ 80 - 0
src/modules/api/fixers/fixers-api.ts

@@ -0,0 +1,80 @@
+import { request } from '../../../utils';
+import { API } from '../../../types';
+import { ResponseType } from '../response-type';
+
+export interface PostGetCountriesReturn extends ResponseType {
+  data: { id: number; country: string; flag: string }[];
+}
+
+export interface PostGetFixersReturn extends ResponseType {
+  data: {
+    id: number;
+    month: number;
+    year: number;
+    contact: string;
+    name: string;
+    email: string;
+    phone: string;
+    web: string;
+    comment: string;
+    added_by_uid: number;
+    added_by_name: string;
+    can_rate: 0 | 1;
+    can_edit: 0 | 1;
+    ratings: Rating[];
+  }[];
+}
+
+type Rating = {
+  rate: string;
+  name: string;
+  comment: string;
+};
+
+export interface PostSaveRating {
+  token: string;
+  fixer_id: number;
+  rating1: number;
+  rating2: number;
+  rating3: number;
+  comment: string;
+}
+
+export interface PostAddFixer {
+  token: string;
+  month: number;
+  year: number;
+  un_ids: number[];
+  name: string;
+  anonymous: 0 | 1;
+  email: string;
+  phone: string;
+  website: string;
+  comment: string;
+}
+
+export interface PostEditFixer {
+  token: string;
+  fixer_id: number;
+  month: number;
+  year: number;
+  un_ids: number[];
+  name: string;
+  anonymous: 0 | 1;
+  email: string;
+  phone: string;
+  website: string;
+  comment: string;
+}
+
+export const fixersApi = {
+  getCountries: (token: string) =>
+    request.postForm<PostGetCountriesReturn>(API.GET_FIXERS_COUNTRIES, { token }),
+  getAllCountries: (token: string) =>
+    request.postForm<PostGetCountriesReturn>(API.GET_ALL_FIXERS_COUNTRIES, { token }),
+  getFixers: (token: string, un_id: number) =>
+    request.postForm<PostGetFixersReturn>(API.GET_FIXERS, { token, un_id }),
+  saveRating: (data: PostSaveRating) => request.postForm<ResponseType>(API.SAVE_RATING, data),
+  addFixer: (data: PostAddFixer) => request.postForm<ResponseType>(API.ADD_FIXER, data),
+  editFixer: (data: PostEditFixer) => request.postForm<ResponseType>(API.EDIT_FIXER, data)
+};

+ 8 - 0
src/modules/api/fixers/fixers-query-keys.tsx

@@ -0,0 +1,8 @@
+export const fixersQueryKeys = {
+  getCountries: (token: string) => ['getCountries', token] as const,
+  getAllCountries: (token: string) => ['getAllCountries', token] as const,
+  getFixers: (id: number) => ['getFixers', id] as const,
+  saveRating: () => ['saveRating'] as const,
+  addFixer: () => ['addFixer'] as const,
+  editFixer: () => ['editFixer'] as const
+};

+ 3 - 0
src/modules/api/fixers/index.ts

@@ -0,0 +1,3 @@
+export * from './queries';
+export * from './fixers-api';
+export * from './fixers-query-keys';

+ 6 - 0
src/modules/api/fixers/queries/index.ts

@@ -0,0 +1,6 @@
+export * from './use-post-get-countries';
+export * from './use-post-get-all-countries';
+export * from './use-post-get-for-country';
+export * from './use-post-save-rating-app';
+export * from './use-post-add-fixer';
+export * from './use-post-edit-fixer';

+ 17 - 0
src/modules/api/fixers/queries/use-post-add-fixer.tsx

@@ -0,0 +1,17 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { fixersQueryKeys } from '../fixers-query-keys';
+import { type PostAddFixer, fixersApi } from '../fixers-api';
+
+import type { BaseAxiosError } from '../../../../types';
+import { ResponseType } from '@api/response-type';
+
+export const usePostAddFixerMutation = () => {
+  return useMutation<ResponseType, BaseAxiosError, PostAddFixer, ResponseType>({
+    mutationKey: fixersQueryKeys.addFixer(),
+    mutationFn: async (data) => {
+      const response = await fixersApi.addFixer(data);
+      return response.data;
+    }
+  });
+};

+ 17 - 0
src/modules/api/fixers/queries/use-post-edit-fixer.tsx

@@ -0,0 +1,17 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { fixersQueryKeys } from '../fixers-query-keys';
+import { type PostEditFixer, fixersApi } from '../fixers-api';
+
+import type { BaseAxiosError } from '../../../../types';
+import { ResponseType } from '@api/response-type';
+
+export const usePostEditFixerMutation = () => {
+  return useMutation<ResponseType, BaseAxiosError, PostEditFixer, ResponseType>({
+    mutationKey: fixersQueryKeys.editFixer(),
+    mutationFn: async (data) => {
+      const response = await fixersApi.editFixer(data);
+      return response.data;
+    }
+  });
+};

+ 17 - 0
src/modules/api/fixers/queries/use-post-get-all-countries.tsx

@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { fixersQueryKeys } from '../fixers-query-keys';
+import { fixersApi, type PostGetCountriesReturn } from '../fixers-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const useGetAllCountriesQuery = (token: string, enabled: boolean) => {
+  return useQuery<PostGetCountriesReturn, BaseAxiosError>({
+    queryKey: fixersQueryKeys.getAllCountries(token),
+    queryFn: async () => {
+      const response = await fixersApi.getAllCountries(token);
+      return response.data;
+    },
+    enabled
+  });
+};

+ 17 - 0
src/modules/api/fixers/queries/use-post-get-countries.tsx

@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { fixersQueryKeys } from '../fixers-query-keys';
+import { fixersApi, type PostGetCountriesReturn } from '../fixers-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const useGetCountriesQuery = (token: string, enabled: boolean) => {
+  return useQuery<PostGetCountriesReturn, BaseAxiosError>({
+    queryKey: fixersQueryKeys.getCountries(token),
+    queryFn: async () => {
+      const response = await fixersApi.getCountries(token);
+      return response.data;
+    },
+    enabled
+  });
+};

+ 17 - 0
src/modules/api/fixers/queries/use-post-get-for-country.tsx

@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { fixersQueryKeys } from '../fixers-query-keys';
+import { fixersApi, type PostGetFixersReturn } from '../fixers-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const useGetFixersQuery = (token: string, id: number, enabled: boolean) => {
+  return useQuery<PostGetFixersReturn, BaseAxiosError>({
+    queryKey: fixersQueryKeys.getFixers(id),
+    queryFn: async () => {
+      const response = await fixersApi.getFixers(token, id);
+      return response.data;
+    },
+    enabled
+  });
+};

+ 17 - 0
src/modules/api/fixers/queries/use-post-save-rating-app.tsx

@@ -0,0 +1,17 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { fixersQueryKeys } from '../fixers-query-keys';
+import { type PostSaveRating, fixersApi } from '../fixers-api';
+
+import type { BaseAxiosError } from '../../../../types';
+import { ResponseType } from '@api/response-type';
+
+export const usePostSaveRatingMutation = () => {
+  return useMutation<ResponseType, BaseAxiosError, PostSaveRating, ResponseType>({
+    mutationKey: fixersQueryKeys.saveRating(),
+    mutationFn: async (data) => {
+      const response = await fixersApi.saveRating(data);
+      return response.data;
+    }
+  });
+};

+ 4 - 1
src/modules/api/friends/queries/use-post-is-notification-active.tsx

@@ -6,7 +6,10 @@ export const fetchFriendsNotification = async (token: string) => {
   try {
     const data: PostGetFriendsNotificationReturn = await queryClient.fetchQuery({
       queryKey: friendsQueryKeys.getNotification(token),
-      queryFn: () => friendsApi.getNotification(token).then((res) => res.data),
+      queryFn: async () => {
+        const response = await friendsApi.getNotification(token);
+        return response.data;
+      },
       gcTime: 0,
       staleTime: 0
     });

+ 5 - 2
src/modules/api/ranking/queries/use-post-get-in-history.tsx

@@ -6,11 +6,14 @@ export const fetchInHistory = async () => {
   try {
     const data: PostGetRanking = await queryClient.fetchQuery({
       queryKey: rankingQueryKeys.getInHistory(),
-      queryFn: () => rankingApi.getInHistory().then((res) => res.data),
+      queryFn: async () => {
+        const response = await rankingApi.getInHistory();
+        return response.data;
+      },
       gcTime: 0,
       staleTime: 0
     });
-    
+
     return data;
   } catch (error) {
     console.error('Failed to fetch in-history data:', error);

+ 4 - 1
src/modules/api/ranking/queries/use-post-get-in-memoriam.tsx

@@ -6,7 +6,10 @@ export const fetchInMemoriam = async () => {
   try {
     const data: PostGetRanking = await queryClient.fetchQuery({
       queryKey: rankingQueryKeys.getInMemoriam(),
-      queryFn: () => rankingApi.getInMemoriam().then((res) => res.data),
+      queryFn: async () => {
+        const response = await rankingApi.getInMemoriam();
+        return response.data;
+      },
       gcTime: 0,
       staleTime: 0
     });

+ 4 - 1
src/modules/api/ranking/queries/use-post-get-limited-ranking.tsx

@@ -6,7 +6,10 @@ export const fetchLimitedRanking = async () => {
   try {
     const data: PostGetRanking = await queryClient.fetchQuery({
       queryKey: rankingQueryKeys.getLimitedRanking(),
-      queryFn: () => rankingApi.getLimitedRanking().then((res) => res.data),
+      queryFn: async () => {
+        const response = await rankingApi.getLimitedRanking();
+        return response.data;
+      },
       gcTime: 0,
       staleTime: 0
     });

+ 5 - 2
src/modules/api/ranking/queries/use-post-get-lpi.tsx

@@ -6,11 +6,14 @@ export const fetchLpi = async () => {
   try {
     const data: PostGetRanking = await queryClient.fetchQuery({
       queryKey: rankingQueryKeys.getLpi(),
-      queryFn: () => rankingApi.getLpi().then((res) => res.data),
+      queryFn: async () => {
+        const response = await rankingApi.getLpi();
+        return response.data;
+      },
       gcTime: 0,
       staleTime: 0
     });
-    
+
     return data;
   } catch (error) {
     console.error('Failed to fetch lpi data:', error);

+ 8 - 2
src/modules/api/ranking/queries/use-post-get-un-masters.tsx

@@ -12,7 +12,10 @@ export const fetchUNMastersTypes = async () => {
   try {
     const data: PostGetUNTypes = await queryClient.fetchQuery({
       queryKey: rankingQueryKeys.getUNMastersTypes(),
-      queryFn: () => rankingApi.getUNMastersTypes().then((res) => res.data),
+      queryFn: async () => {
+        const response = await rankingApi.getUNMastersTypes();
+        return response.data;
+      },
       gcTime: 0,
       staleTime: 0
     });
@@ -27,7 +30,10 @@ export const fetchUNMastersType = async (type: number) => {
   try {
     const data: PostGetUNType | CountryUNType | YearUNType = await queryClient.fetchQuery({
       queryKey: rankingQueryKeys.getUNMastersType(type),
-      queryFn: () => rankingApi.getUNMastersType(type).then((res) => res.data),
+      queryFn: async () => {
+        const response = await rankingApi.getUNMastersType(type);
+        return response.data;
+      },
       gcTime: 0,
       staleTime: 0
     });

+ 1 - 0
src/modules/api/response-type.ts

@@ -12,4 +12,5 @@ export interface ResponseType {
   result: ResultTypes;
   status?: StatusTypes;
   result_description?: string;
+  login_needed?: number;
 }

+ 4 - 1
src/modules/api/series/queries/use-post-get-items-for-series.tsx

@@ -7,7 +7,10 @@ export const fetchItemsForSeries = async (token: string, series_id: string) => {
   try {
     const data: PostGetItems = await queryClient.fetchQuery({
       queryKey: seriesQueryKeys.getItemsForSeries(token, series_id),
-      queryFn: () => seriesApi.getItemsForSeries(token, series_id).then((res) => res.data),
+      queryFn: async () => {
+        const response = await seriesApi.getItemsForSeries(token, series_id);
+        return response.data;
+      },
       gcTime: 0,
       staleTime: 0
     });

+ 8 - 2
src/modules/api/statistics/queries/use-post-get-statistics.tsx

@@ -6,7 +6,10 @@ export const fetchList = async (token: string) => {
   try {
     const data: PostGetList = await queryClient.fetchQuery({
       queryKey: statisticsQueryKeys.getList(token),
-      queryFn: () => statisticsApi.getList(token).then((res) => res.data),
+      queryFn: async () => {
+        const response = await statisticsApi.getList(token);
+        return response.data;
+      },
       gcTime: 0,
       staleTime: 0
     });
@@ -21,7 +24,10 @@ export const fetchStatistic = async (token: string, url1: string, url2: string)
   try {
     const data: PostGetStat = await queryClient.fetchQuery({
       queryKey: statisticsQueryKeys.getStatistic(token, url1, url2),
-      queryFn: () => statisticsApi.getStatistic(token, url1, url2).then((res) => res.data),
+      queryFn: async () => {
+        const response = await statisticsApi.getStatistic(token, url1, url2);
+        return response.data;
+      },
       gcTime: 0,
       staleTime: 0
     });

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

@@ -7,3 +7,4 @@ 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-save-notification-token';
+export * from './use-post-get-update';

+ 2 - 2
src/modules/api/user/queries/use-post-get-map-years.tsx

@@ -5,11 +5,11 @@ import { type PostGetMapYearsReturn, userApi } from '../user-api';
 
 import type { BaseAxiosError } from '../../../../types';
 
-export const usePostGetMapYearsQuery = (userId: number, enabled: boolean) => {
+export const usePostGetMapYearsQuery = (token: string, userId: number, enabled: boolean) => {
   return useQuery<PostGetMapYearsReturn, BaseAxiosError>({
     queryKey: userQueryKeys.getMapYears(userId),
     queryFn: async () => {
-      const response = await userApi.getMapYears(userId);
+      const response = await userApi.getMapYears(token, userId);
       return response.data;
     },
     enabled

+ 2 - 2
src/modules/api/user/queries/use-post-get-profile-regions.tsx

@@ -5,11 +5,11 @@ import { type PostGetProfileRegionsReturn, userApi } from '../user-api';
 
 import type { BaseAxiosError } from '../../../../types';
 
-export const usePostGetProfileRegions = (uid: number, type: string) => {
+export const usePostGetProfileRegions = (token: string, uid: number, type: string) => {
   return useQuery<PostGetProfileRegionsReturn, BaseAxiosError>({
     queryKey: userQueryKeys.getProfileRegions(uid, type),
     queryFn: async () => {
-      const response = await userApi.getProfileRegions(uid, type);
+      const response = await userApi.getProfileRegions(token, uid, type);
       return response.data;
     }
   });

+ 5 - 1
src/modules/api/user/queries/use-post-get-profile-updates.tsx

@@ -5,7 +5,11 @@ import { type PostGetProfileUpdatesReturn, userApi } from '../user-api';
 
 import type { BaseAxiosError } from '../../../../types';
 
-export const usePostGetProfileUpdatesQuery = (token: string, userId: number, enabled: boolean) => {
+export const usePostGetProfileUpdatesQuery = (
+  token: string,
+  userId: number,
+  enabled: boolean
+) => {
   return useQuery<PostGetProfileUpdatesReturn, BaseAxiosError>({
     queryKey: userQueryKeys.getProfileUpdates(userId),
     queryFn: async () => {

+ 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
+  });
+};

+ 68 - 14
src/modules/api/user/user-api.tsx

@@ -267,16 +267,16 @@ export interface PostGetProfileDataReturn extends ResponseType {
 export interface PostGetProfileUpdatesReturn extends ResponseType {
   data: {
     can_see_updates: 0 | 1;
-    friends_total: number;
     updates: {
-      countries: number;
-      dare: number;
-      friends: number;
-      new_nm: number;
-      photos: number;
-      series: number;
-      visited_regions: number;
-      whs: number;
+      un_visited: number;
+      un_new: number;
+      unp_visited: number;
+      unp_new: number;
+      nm_visited: number;
+      nm_new: number;
+      new_dare: number;
+      new_series: number;
+      new_whs: number;
     };
   };
 }
@@ -293,6 +293,58 @@ 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;
+    id: number
+  }[];
+}
+
+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 }),
@@ -313,8 +365,8 @@ export const userApi = {
     request.postForm<Exclude<PostGetProfileInfoReturn, { email: null }>>(API.PROFILE_INFO_PUBLIC, {
       uid
     }),
-  getProfileRegions: (uid: number, type: string) =>
-    request.postForm<PostGetProfileRegionsReturn>(API.GET_PROFILE_REGIONS, { uid, type }),
+  getProfileRegions: (token: string, uid: number, type: string) =>
+    request.postForm<PostGetProfileRegionsReturn>(API.GET_PROFILE_REGIONS, { token, uid, type }),
   getProfileInfoData: (token: string, profile_id: number) =>
     request.postForm<PostGetProfileDataReturn>(API.GET_PROGILE_DATA, {
       token,
@@ -325,12 +377,14 @@ export const userApi = {
       token,
       profile_id
     }),
-  getMapYears: (profile_id: number) =>
-    request.postForm<PostGetMapYearsReturn>(API.GET_MAP_YEARS, { profile_id }),
   setNotificationToken: (token: string, platform: string, n_token: string) =>
     request.postForm<ResponseType>(API.SET_NOTIFICATION_TOKEN, {
       token,
       platform,
       n_token
-    })
+    }),
+  getMapYears: (token: string, profile_id: number) =>
+    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

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

+ 2 - 2
src/screens/InAppScreens/MapScreen/ClusterItem/index.tsx

@@ -6,8 +6,8 @@ const ClusterItem = ({ cluster }: { cluster: any }) => {
   return (
     <Marker
       coordinate={{
-        latitude: cluster.geometry.coordinates[1],
-        longitude: cluster.geometry.coordinates[0]
+        latitude: cluster.geometry.coordinates[1] ?? 0,
+        longitude: cluster.geometry.coordinates[0] ?? 0
       }}
     >
       <View style={styles.clusterContainer}>

+ 35 - 1
src/screens/InAppScreens/MapScreen/CountryViewScreen/index.tsx

@@ -3,7 +3,7 @@ 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 { CommonActions, useFocusEffect } from '@react-navigation/native';
 import { Colors } from 'src/theme';
 import { ScrollView } from 'react-native-gesture-handler';
 
@@ -28,6 +28,7 @@ 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';
+import MapSvg from 'assets/icons/travels-screens/map-location.svg';
 
 const CountryViewScreen: FC<Props> = ({ navigation, route }) => {
   const countryId = route.params?.regionId;
@@ -94,6 +95,9 @@ const CountryViewScreen: FC<Props> = ({ navigation, route }) => {
 
         setSeries(data?.data?.series || []);
         setRoutes(staticGroups);
+        if (regionData?.id !== countryId) {
+          setRegionData(data?.data || {});
+        }
         setIsLoading(false);
       };
 
@@ -220,6 +224,7 @@ const CountryViewScreen: FC<Props> = ({ navigation, route }) => {
           <ChevronLeft fill={Colors.WHITE} />
         </View>
       </TouchableOpacity>
+
       <ScrollView
         contentContainerStyle={{ flexGrow: 1 }}
         nestedScrollEnabled={true}
@@ -241,6 +246,35 @@ const CountryViewScreen: FC<Props> = ({ navigation, route }) => {
             <Text style={styles.emptyImageText}>No image available at this location</Text>
           </View>
         )}
+        <TouchableOpacity
+          onPress={() => {
+            route.params?.isTravelsScreen || route.params?.isProfileScreen
+              ? navigation.dispatch(
+                  CommonActions.reset({
+                    index: 1,
+                    routes: [
+                      {
+                        name: NAVIGATION_PAGES.IN_APP_MAP_TAB,
+                        state: {
+                          routes: [
+                            {
+                              name: NAVIGATION_PAGES.MAP_TAB,
+                              params: { id: countryId, type: 'countries' }
+                            }
+                          ]
+                        }
+                      }
+                    ]
+                  })
+                )
+              : navigation.goBack();
+          }}
+          style={styles.goToMapBtn}
+        >
+          <View style={styles.chevronWrapper}>
+            <MapSvg fill={Colors.WHITE} />
+          </View>
+        </TouchableOpacity>
 
         <View style={styles.wrapper}>
           {regionData?.visited && !disabled && (

+ 3 - 2
src/screens/InAppScreens/MapScreen/FilterModal/index.tsx

@@ -42,6 +42,7 @@ const FilterModal = ({
   isPublicView: boolean;
   isLogged: boolean;
 }) => {
+  const token = storage.get('token', StoreType.STRING) as string;
   const [index, setIndex] = useState(0);
   const [selectedYear, setSelectedYear] = useState<{ label: string; value: number } | null>(null);
   const [allYears, setAllYears] = useState<{ label: string; value: number }[]>([]);
@@ -54,7 +55,7 @@ const FilterModal = ({
     { key: 'regions', title: 'Travels' },
     { key: 'series', title: 'Series' }
   ]);
-  const { data } = usePostGetMapYearsQuery(userId, isLogged ? true : false);
+  const { data } = usePostGetMapYearsQuery(token as string, userId, isLogged ? true : false);
   const { data: seriesList } = useGetListQuery(true);
   const [series, setSeries] = useState<{ label: string; value: number }[]>([]);
   const [selectedSeries, setSelectedSeries] = useState<number[]>([]);
@@ -109,7 +110,7 @@ const FilterModal = ({
   };
 
   useEffect(() => {
-    if (data) {
+    if (data && data.data && data.data.map_years) {
       const years = data.data.map_years.filter((year) => year > 1900);
       const formattedYears = years
         .map((year) => ({ label: year.toString(), value: year }))

+ 1 - 1
src/screens/InAppScreens/MapScreen/MarkerItem/index.tsx

@@ -19,7 +19,7 @@ const MarkerItem = ({
 }: {
   marker: ItemSeries;
   iconUrl: string;
-  coordinate?: any;
+  coordinate: { latitude: number; longitude: number };
   seriesName: string;
   toggleSeries: (item: any) => void;
   token: string;

+ 101 - 1
src/screens/InAppScreens/MapScreen/RegionViewScreen/index.tsx

@@ -9,9 +9,10 @@ import {
   Loading,
   Modal as ReactModal
 } from 'src/components';
-import { useFocusEffect } from '@react-navigation/native';
+import { CommonActions, useFocusEffect } from '@react-navigation/native';
 import { Colors } from 'src/theme';
 import { styles as ButtonStyles } from 'src/components/RegionPopup/style';
+import * as ImagePicker from 'expo-image-picker';
 
 import { usePostSetToggleItem } from '@api/series';
 import { ScrollView } from 'react-native-gesture-handler';
@@ -35,6 +36,9 @@ import HouseSvg from 'assets/icons/house.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 MapSvg from 'assets/icons/travels-screens/map-location.svg';
+import AddImgSvg from 'assets/icons/travels-screens/add-img.svg';
+import { useGetTempQuery } from '@api/photos';
 
 const RegionViewScreen: FC<Props> = ({ navigation, route }) => {
   const regionId = route.params?.regionId;
@@ -66,6 +70,7 @@ const RegionViewScreen: FC<Props> = ({ navigation, route }) => {
     years: [],
     id: regionId
   });
+  const { data: tempData, refetch } = useGetTempQuery(token, token ? true : false);
 
   const {
     handleUpdateNM: updateNM,
@@ -122,6 +127,9 @@ const RegionViewScreen: FC<Props> = ({ navigation, route }) => {
         setName([regionName, subname]);
         setSeries(data?.data?.series || []);
         setRoutes(staticGroups);
+        if (regionData?.id !== regionId) {
+          setRegionData(data?.data || {});
+        }
         setIsLoading(false);
       };
 
@@ -270,6 +278,59 @@ const RegionViewScreen: FC<Props> = ({ navigation, route }) => {
     });
   };
 
+  const handleImagePick = async () => {
+    setIsLoading(true);
+
+    const photosData = {
+      id: regionId,
+      country: name[0],
+      name: name[1],
+      flag: '',
+      nm: type === 'nm',
+      dare: type === 'dare',
+      photos: []
+    };
+
+    await refetch().then(async (temp) => {
+      if (temp.data?.photos && temp.data?.photos.length > 0) {
+        setIsLoading(false);
+        navigation.navigate(
+          ...([
+            NAVIGATION_PAGES.ADD_PHOTO,
+            {
+              data: photosData,
+              images: [],
+              allRegions: [],
+              tempData: temp.data.photos
+            }
+          ] as never)
+        );
+      } else {
+        setIsLoading(false);
+
+        let result = await ImagePicker.launchImageLibraryAsync({
+          mediaTypes: ImagePicker.MediaTypeOptions.Images,
+          quality: 1,
+          allowsMultipleSelection: true,
+          exif: true
+        });
+
+        if (!result.canceled) {
+          navigation.navigate(
+            ...([
+              NAVIGATION_PAGES.ADD_PHOTO,
+              {
+                data: photosData,
+                images: result.assets,
+                allRegions: []
+              }
+            ] as never)
+          );
+        }
+      }
+    });
+  };
+
   return (
     <View style={styles.container}>
       <TouchableOpacity
@@ -282,6 +343,7 @@ const RegionViewScreen: FC<Props> = ({ navigation, route }) => {
           <ChevronLeft fill={Colors.WHITE} />
         </View>
       </TouchableOpacity>
+
       <ScrollView
         contentContainerStyle={{ flexGrow: 1 }}
         nestedScrollEnabled={true}
@@ -303,6 +365,43 @@ const RegionViewScreen: FC<Props> = ({ navigation, route }) => {
             <Text style={styles.emptyImageText}>No image available at this location</Text>
           </View>
         )}
+        {regionData?.visited ? (
+          <TouchableOpacity onPress={handleImagePick} style={styles.addPhotoButton}>
+            <View style={styles.chevronWrapper}>
+              <AddImgSvg fill={Colors.WHITE} width={20} height={20} />
+            </View>
+          </TouchableOpacity>
+        ) : null}
+
+        <TouchableOpacity
+          onPress={() => {
+            route.params?.isTravelsScreen || route.params?.isProfileScreen
+              ? navigation.dispatch(
+                  CommonActions.reset({
+                    index: 1,
+                    routes: [
+                      {
+                        name: NAVIGATION_PAGES.IN_APP_MAP_TAB,
+                        state: {
+                          routes: [
+                            {
+                              name: NAVIGATION_PAGES.MAP_TAB,
+                              params: { id: regionId, type: type === 'nm' ? 'regions' : 'places' }
+                            }
+                          ]
+                        }
+                      }
+                    ]
+                  })
+                )
+              : navigation.goBack();
+          }}
+          style={styles.goToMapBtn}
+        >
+          <View style={styles.chevronWrapper}>
+            <MapSvg fill={Colors.WHITE} />
+          </View>
+        </TouchableOpacity>
 
         <View style={styles.wrapper}>
           <View style={{ flexDirection: 'row', gap: 8, justifyContent: 'flex-end' }}>
@@ -466,6 +565,7 @@ const RegionViewScreen: FC<Props> = ({ navigation, route }) => {
                 setIndex={setIndexSeries}
                 routes={routes}
                 renderScene={({ route }: { route: SeriesGroup }) => <View style={{ height: 0 }} />}
+                maxTabHeight={50}
               />
               <TravelSeriesList
                 series={series}

+ 23 - 3
src/screens/InAppScreens/MapScreen/RegionViewScreen/styles.tsx

@@ -14,14 +14,14 @@ export const styles = StyleSheet.create({
     borderRadius: 21,
     justifyContent: 'center',
     alignItems: 'center',
-    backgroundColor: 'rgba(15, 63, 79, 0.6)'
+    backgroundColor: 'rgba(0, 0, 0, 0.3)'
   },
   backButton: {
     position: 'absolute',
     width: 50,
     height: 50,
     top: 50,
-    left: 5,
+    left: 10,
     justifyContent: 'center',
     alignItems: 'center',
     zIndex: 2
@@ -191,5 +191,25 @@ export const styles = StyleSheet.create({
     color: Colors.DARK_BLUE,
     fontWeight: '600',
     fontSize: getFontSize(11)
-  }
+  },
+  goToMapBtn: {
+    position: 'absolute',
+    width: 50,
+    height: 50,
+    top: 50,
+    right: 10,
+    justifyContent: 'center',
+    alignItems: 'center',
+    zIndex: 2
+  },
+  addPhotoButton: {
+    position: 'absolute',
+    width: 50,
+    height: 50,
+    top: 160,
+    right: 10,
+    justifyContent: 'center',
+    alignItems: 'center',
+    zIndex: 2
+  },
 });

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

@@ -235,7 +235,7 @@ const UsersListScreen: FC<Props> = ({ navigation, route }) => {
             setUsers(data?.data?.users);
             setSelectedUsers(data?.data?.users);
             setMaxPages(data?.data?.max_pages);
-            !masterCountries.length && setMasterCountries(convertData(data?.data?.countries) ?? []);
+            !masterCountries?.length && setMasterCountries(convertData(data?.data?.countries) ?? []);
             setLoading(false);
           }
         }
@@ -254,7 +254,7 @@ const UsersListScreen: FC<Props> = ({ navigation, route }) => {
             setUsers(data?.data?.users);
             setSelectedUsers(data?.data?.users);
             setMaxPages(data?.data?.max_pages);
-            !masterCountries.length && setMasterCountries(convertData(data?.data?.countries) ?? []);
+            !masterCountries?.length && setMasterCountries(convertData(data?.data?.countries) ?? []);
             setLoading(false);
           }
         }
@@ -273,7 +273,7 @@ const UsersListScreen: FC<Props> = ({ navigation, route }) => {
             setUsers(data?.data?.users);
             setSelectedUsers(data?.data?.users);
             setMaxPages(data?.data?.max_pages);
-            !masterCountries.length && setMasterCountries(convertData(data?.data?.countries) ?? []);
+            !masterCountries?.length && setMasterCountries(convertData(data?.data?.countries) ?? []);
             setLoading(false);
           }
         }

+ 175 - 87
src/screens/InAppScreens/MapScreen/index.tsx

@@ -74,6 +74,13 @@ const localDareDir = `${FileSystem.cacheDirectory}tiles/regions_mqp`;
 
 const AnimatedMarker = Animation.createAnimatedComponent(Marker);
 
+const INITIAL_REGION = {
+  latitude: 0,
+  longitude: 0,
+  latitudeDelta: 180,
+  longitudeDelta: 180
+};
+
 const MapScreen: React.FC<MapScreenProps> = ({ navigation, route }) => {
   const [dareData, setDareData] = useState(jsonData);
   const tilesBaseURL = `${FASTEST_MAP_HOST}/tiles_osm`;
@@ -141,6 +148,8 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation, route }) => {
   const savedVisitedTilesUrl = storage.get('visitedTilesUrl', StoreType.STRING) as string;
   const [userInfoData, setUserInfoData] = useState<any>(null);
 
+  const [initialRegion, setInitialRegion] = useState(INITIAL_REGION);
+
   useFocusEffect(
     useCallback(() => {
       const updateMarkers = async () => {
@@ -164,6 +173,27 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation, route }) => {
     }, [userData])
   );
 
+  useEffect(() => {
+    const loadInitialRegion = () => {
+      try {
+        const savedInitialRegion = storage.get('initialRegion', StoreType.STRING) as string;
+        if (savedInitialRegion) {
+          const region = JSON.parse(savedInitialRegion);
+          setInitialRegion(region);
+
+          const currentZoom = Math.log2(360 / region.latitudeDelta);
+          if (currentZoom >= 7) {
+            findFeaturesInVisibleMapArea(region);
+          }
+        }
+      } catch (e) {
+        console.error('Failed to load saved initial region:', e);
+      }
+    };
+
+    loadInitialRegion();
+  }, [dareData]);
+
   useEffect(() => {
     if (route.params?.id && route.params?.type && dareData) {
       handleFindRegion(route.params?.id, route.params?.type);
@@ -283,17 +313,21 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation, route }) => {
         return;
       }
 
-      let currentLocation = await Location.getCurrentPositionAsync({});
+      let currentLocation = await Location.getCurrentPositionAsync({
+        accuracy: Location.Accuracy.Low
+      });
       setLocation(currentLocation.coords);
     })();
   }, []);
 
   const findFeaturesInVisibleMapArea = async (visibleMapArea: {
-    latitude?: any;
-    longitude?: any;
+    latitude: any;
+    longitude: any;
     latitudeDelta: any;
-    longitudeDelta?: any;
+    longitudeDelta: any;
   }) => {
+    storage.set('initialRegion', JSON.stringify(visibleMapArea));
+
     if (!isConnected) return;
     const currentZoom = Math.log2(360 / visibleMapArea.latitudeDelta);
     setZoomLevel(currentZoom);
@@ -362,15 +396,17 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation, route }) => {
     });
     setLocation(currentLocation.coords);
 
-    mapRef.current?.animateToRegion(
-      {
-        latitude: currentLocation.coords.latitude,
-        longitude: currentLocation.coords.longitude,
-        latitudeDelta: 5,
-        longitudeDelta: 5
-      },
-      800
-    );
+    currentLocation.coords?.latitude &&
+      currentLocation.coords?.longitude &&
+      mapRef.current?.animateToRegion(
+        {
+          latitude: currentLocation.coords.latitude,
+          longitude: currentLocation.coords.longitude,
+          latitudeDelta: 5,
+          longitudeDelta: 5
+        },
+        800
+      );
 
     handleClosePopup();
   };
@@ -394,7 +430,11 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation, route }) => {
   const handleMapPress = async (event: {
     nativeEvent: { coordinate: { latitude: any; longitude: any }; action?: string };
   }) => {
-    if (event.nativeEvent?.action === 'marker-press') return;
+    if (
+      event.nativeEvent?.action === 'marker-press' ||
+      event.nativeEvent?.action === 'callout-inside-press'
+    )
+      return;
 
     cancelTokenRef.current = true;
     const { latitude, longitude } = event.nativeEvent.coordinate;
@@ -404,10 +444,10 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation, route }) => {
     let tableName = 'places';
     let foundRegion: any;
 
-    type !== 1 && (foundRegion = findRegionInDataset(dareData, point));
+    type !== 1 && (foundRegion = dareData ? findRegionInDataset(dareData, point) : null);
 
     if (type === 1) {
-      foundRegion = findRegionInDataset(regions, point);
+      foundRegion = regions ? findRegionInDataset(regions, point) : null;
       db = getFirstDatabase();
       tableName = 'regions';
 
@@ -433,19 +473,20 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation, route }) => {
               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);
+          token &&
+            (await mutateCountriesData(
+              { id: +countryId, token },
+              {
+                onSuccess: (data) => {
+                  setUserData({ type: 'countries', id: +countryId, ...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;
         }
@@ -453,7 +494,7 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation, route }) => {
     }
 
     if (!foundRegion) {
-      foundRegion = findRegionInDataset(regions, point);
+      foundRegion = regions ? findRegionInDataset(regions, point) : null;
       db = getFirstDatabase();
       tableName = 'regions';
     }
@@ -492,34 +533,37 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation, route }) => {
       zoomLevel < 7 && mapRef.current?.animateToRegion(region, 1000);
 
       if (tableName === 'regions') {
-        await mutateUserData(
-          { region_id: +id, token: String(token) },
-          {
-            onSuccess: (data) => {
-              setUserData({ type: 'nm', ...data });
+        token &&
+          (await mutateUserData(
+            { region_id: +id, token: String(token) },
+            {
+              onSuccess: (data) => {
+                setUserData({ type: 'nm', id: +id, ...data });
+              }
             }
-          }
-        );
-        await mutateAsync(
-          { regions: JSON.stringify([id]), token: String(token) },
-          {
-            onSuccess: (data) => {
-              setSeries(data.series);
+          ));
+        token &&
+          (await mutateAsync(
+            { regions: JSON.stringify([id]), token: String(token) },
+            {
+              onSuccess: (data) => {
+                setSeries(data.series);
 
-              const allMarkers = data.items.map(processMarkerData);
-              setProcessedMarkers(allMarkers);
+                const allMarkers = data.items.map(processMarkerData);
+                setProcessedMarkers(allMarkers);
+              }
             }
-          }
-        );
+          ));
       } else {
-        await mutateUserDataDare(
-          { dare_id: +id, token: String(token) },
-          {
-            onSuccess: (data) => {
-              setUserData({ type: 'dare', ...data });
+        token &&
+          (await mutateUserDataDare(
+            { dare_id: +id, token: String(token) },
+            {
+              onSuccess: (data) => {
+                setUserData({ type: 'dare', id: +id, ...data });
+              }
             }
-          }
-        );
+          ));
         setProcessedMarkers([]);
       }
     } else {
@@ -529,10 +573,49 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation, route }) => {
 
   const handleFindRegion = async (id: number, type: string) => {
     cancelTokenRef.current = true;
-    const db = type === 'regions' ? getFirstDatabase() : getSecondDatabase();
-    const dataset = type === 'regions' ? regions : dareData;
+    const db =
+      type === 'regions'
+        ? getFirstDatabase()
+        : type === 'countries'
+          ? getCountriesDatabase()
+          : getSecondDatabase();
+
+    if (type === 'countries') {
+      setSelectedRegion(null);
+      setMarkers([]);
+      setProcessedMarkers([]);
 
-    const foundRegion = dataset.features.find((region: any) => region.properties.id === id);
+      await getData(db, id, type, handleRegionData)
+        .then(() => {
+          setRegionPopupVisible(true);
+        })
+        .catch((error) => {
+          console.error('Error fetching data', error);
+          refreshDatabases();
+        });
+
+      token &&
+        (await mutateCountriesData(
+          { id: +id, token },
+          {
+            onSuccess: (data) => {
+              setUserData({ type: 'countries', id: +id, ...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;
+    }
+
+    const dataset = type === 'regions' ? regions : dareData;
+    const foundRegion = dataset
+      ? dataset.features.find((region: any) => region.properties.id === id)
+      : null;
 
     if (foundRegion) {
       setSelectedRegion({
@@ -565,35 +648,38 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation, route }) => {
       mapRef.current?.animateToRegion(region, 1000);
 
       if (type === 'regions') {
-        await mutateUserData(
-          { region_id: +id, token: String(token) },
-          {
-            onSuccess: (data) => {
-              setUserData({ type: 'nm', ...data });
+        token &&
+          (await mutateUserData(
+            { region_id: +id, token: String(token) },
+            {
+              onSuccess: (data) => {
+                setUserData({ type: 'nm', id: +id, ...data });
+              }
             }
-          }
-        );
-        await mutateAsync(
-          { regions: JSON.stringify([id]), token: String(token) },
-          {
-            onSuccess: (data) => {
-              setSeries(data.series);
+          ));
+        token &&
+          (await mutateAsync(
+            { regions: JSON.stringify([id]), token: String(token) },
+            {
+              onSuccess: (data) => {
+                setSeries(data.series);
 
-              const allMarkers = data.items.map(processMarkerData);
-              setProcessedMarkers(allMarkers);
-              setMarkers(allMarkers);
+                const allMarkers = data.items.map(processMarkerData);
+                setProcessedMarkers(allMarkers);
+                setMarkers(allMarkers);
+              }
             }
-          }
-        );
+          ));
       } else {
-        await mutateUserDataDare(
-          { dare_id: +id, token: String(token) },
-          {
-            onSuccess: (data) => {
-              setUserData({ type: 'dare', ...data });
+        token &&
+          (await mutateUserDataDare(
+            { dare_id: +id, token: String(token) },
+            {
+              onSuccess: (data) => {
+                setUserData({ type: 'dare', id: +id, ...data });
+              }
             }
-          }
-        );
+          ));
         setProcessedMarkers([]);
         setMarkers([]);
       }
@@ -614,6 +700,7 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation, route }) => {
       offlineMode={!isConnected}
       opacity={opacity}
       zIndex={zIndex}
+      tileSize={256}
     />
   );
 
@@ -756,12 +843,7 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation, route }) => {
   return (
     <View style={styles.container}>
       <ClusteredMapView
-        initialRegion={{
-          latitude: 0,
-          longitude: 0,
-          latitudeDelta: 180,
-          longitudeDelta: 180
-        }}
+        region={initialRegion}
         ref={mapRef}
         showsMyLocationButton={false}
         showsCompass={false}
@@ -841,7 +923,13 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation, route }) => {
               handleUpdateDare(id, visits);
             }}
             disabled={!token || !isConnected}
-            updateSlow={handleUpdateSlow}
+            updateSlow={(id, v, s11, s31, s101) => {
+              if (!token) {
+                setIsWarningModalVisible(true);
+                return;
+              }
+              handleUpdateSlow(id, v, s11, s31, s101);
+            }}
             openEditSlowModal={handleOpenEditSlowModal}
           />
         </>
@@ -955,7 +1043,7 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation, route }) => {
         setVisitedTiles={setVisitedTiles}
         setSeriesFilter={setSeriesFilter}
         isPublicView={false}
-        isLogged={!!token}
+        isLogged={token ? true : false}
       />
       <EditModal
         isVisible={isEditSlowModalVisible}

+ 111 - 88
src/screens/InAppScreens/ProfileScreen/Components/PersonalInfo.tsx

@@ -1,19 +1,18 @@
 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';
 import RegionsRenderer from '../RegionsRenderer';
 
 import CompassIcon from 'assets/icons/travels-section/compass.svg';
-import FriendsIcon from 'assets/icons/user-group.svg';
 import FlagsIcon from 'assets/icons/travels-section/flags.svg';
-import PhotosIcon from 'assets/icons/travels-section/images.svg';
 import RegionsIcon from 'assets/icons/travels-section/regions.svg';
 import SeriesIcon from 'assets/icons/travels-section/series.svg';
 import WHSIcon from 'assets/icons/travels-section/whs.svg';
 import ArrowIcon from 'assets/icons/next.svg';
+import UNPIcon from 'assets/icons/travels-section/unp.svg';
 
 import { styles } from './styles';
 import { InfoItem } from './InfoItem';
@@ -23,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: {
@@ -46,15 +46,16 @@ type PersonalInfoProps = {
     ownProfile: 0 | 1;
   };
   updates: {
-    countries: number;
-    dare: number;
-    friends: number;
-    new_nm: number;
-    photos: number;
-    series: number;
-    visited_regions: number;
-    whs: number;
-  };
+    un_visited: number;
+    un_new: number;
+    unp_visited: number;
+    unp_new: number;
+    nm_visited: number;
+    nm_new: number;
+    new_dare: number;
+    new_series: number;
+    new_whs: number;
+  } | null;
   userId: number;
   navigation: NavigationProp<any>;
   isPublicView: boolean;
@@ -88,8 +89,11 @@ export const PersonalInfo: FC<PersonalInfoProps> = ({
     action: () => {},
     title: ''
   });
+  const [isUpdatesModalVisible, setIsUpdatesModalVisible] = useState(false);
+  const [updateType, setUpdateType] = useState<string>('nm');
 
-  const { data: regions } = usePostGetProfileRegions(userId, type);
+  const { data: regions } = usePostGetProfileRegions(token as string, userId, type);
+  const { data: update } = usePostGetUpdateQuery(token as string, userId, updateType, true);
 
   useEffect(() => {
     if (data.isFriend === 1) {
@@ -151,15 +155,15 @@ export const PersonalInfo: FC<PersonalInfoProps> = ({
   };
 
   const hasUpdates = () => {
+    if (!updates) return false;
+
     return (
-      (updates.countries && updates.countries > 0) ||
-      (updates.visited_regions && updates.visited_regions > 0) ||
-      (updates.dare && updates.dare > 0) ||
-      (updates.series && updates.series > 0) ||
-      (updates.whs && updates.whs > 0) ||
-      (updates.new_nm && updates.new_nm > 0) ||
-      (updates.photos && updates.photos > 0) ||
-      (updates.friends && updates.friends > 0)
+      (updates.un_visited && updates.un_visited > 0) ||
+      (updates.unp_visited && updates.unp_visited > 0) ||
+      (updates.nm_visited && updates.nm_visited > 0) ||
+      (updates.new_dare && updates.new_dare > 0) ||
+      (updates.new_series && updates.new_series > 0) ||
+      (updates.new_whs && updates.new_whs > 0)
     );
   };
 
@@ -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;
@@ -228,7 +237,7 @@ export const PersonalInfo: FC<PersonalInfoProps> = ({
         ) : null}
 
         {data.friends.length > 0 ? (
-          <InfoItem inline={true} title={'FRIENDS'}>
+          <InfoItem inline={true} title={`FRIENDS (${data.friends.length})`}>
             <View style={{ flexDirection: 'row', flex: 1 }}>
               {data.friends.slice(0, maxAvatars).map((friend, index) => (
                 <Tooltip
@@ -267,7 +276,7 @@ export const PersonalInfo: FC<PersonalInfoProps> = ({
                       />
                     ) : (
                       <AvatarWithInitials
-                        text={`${friend.first_name[0] ?? ''}${friend.last_name[0] ?? ''}`}
+                        text={`${friend?.first_name ? friend.first_name[0] : ''}${friend?.last_name ? friend.last_name[0] : ''}`}
                         flag={API_HOST + '/img/flags_new/' + friend.flag}
                         size={28}
                         borderColor={Colors.DARK_LIGHT}
@@ -317,80 +326,79 @@ export const PersonalInfo: FC<PersonalInfoProps> = ({
           </InfoItem>
         ) : null}
 
-        {hasUpdates() ? (
-          <InfoItem title={'UPDATES (last 90 days)'}>
+        {updates && hasUpdates() ? (
+          <InfoItem title={'VISITED IN THE LAST 90 DAYS'}>
             <View style={{ flexDirection: 'row', flexWrap: 'wrap' }}>
-              {updates.countries && updates.countries > 0 ? (
-                <View style={styles.updates}>
+              {updates.nm_visited && updates.nm_visited > 0 ? (
+                <TouchableOpacity style={styles.updates} onPress={() => handleOpenUpdates('nm')}>
+                  <RegionsIcon fill={Colors.DARK_BLUE} height={20} width={20} />
+                  {updates.nm_new && updates.nm_new > 0 ? (
+                    <View>
+                      <Text style={styles.updatesText}>
+                        {updates.nm_visited} (+{updates.nm_new} new)
+                      </Text>
+                      <Text style={styles.updatesText}>NM regions</Text>
+                    </View>
+                  ) : (
+                    <Text style={styles.updatesText}>{updates.nm_visited} NM regions</Text>
+                  )}
+                </TouchableOpacity>
+              ) : null}
+
+              {updates.un_visited && updates.un_visited > 0 ? (
+                <TouchableOpacity style={styles.updates} onPress={() => handleOpenUpdates('un')}>
                   <FlagsIcon fill={Colors.DARK_BLUE} height={20} width={20} />
-                  <View>
-                    <Text style={styles.updatesTextCount}>+{updates.countries}</Text>
-                    <Text style={styles.updatesText}>visited countries</Text>
-                  </View>
-                </View>
+                  {updates.un_new && updates.un_new > 0 ? (
+                    <View>
+                      <Text style={styles.updatesText}>
+                        {updates.un_visited} (+{updates.un_new} new)
+                      </Text>
+                      <Text style={styles.updatesText}>UN countries</Text>
+                    </View>
+                  ) : (
+                    <Text style={styles.updatesText}>{updates.un_visited} UN countries</Text>
+                  )}
+                </TouchableOpacity>
               ) : null}
-              {updates.visited_regions && updates.visited_regions > 0 ? (
-                <View style={styles.updates}>
-                  <RegionsIcon fill={Colors.DARK_BLUE} height={20} width={20} />
-                  <View>
-                    <Text style={styles.updatesTextCount}>+{updates.visited_regions}</Text>
-                    <Text style={styles.updatesText}>visited regions</Text>
-                  </View>
-                </View>
+
+              {updates.unp_visited && updates.unp_visited > 0 ? (
+                <TouchableOpacity style={styles.updates} onPress={() => handleOpenUpdates('unp')}>
+                  <UNPIcon fill={Colors.DARK_BLUE} height={20} width={20} />
+                  {updates.unp_new && updates.unp_new > 0 ? (
+                    <View>
+                      <Text style={styles.updatesText}>
+                        {updates.unp_visited} (+{updates.unp_new} new)
+                      </Text>
+                      <Text style={styles.updatesText}>UN+ states</Text>
+                    </View>
+                  ) : (
+                    <Text style={styles.updatesText}>{updates.unp_visited} UN+ states</Text>
+                  )}
+                </TouchableOpacity>
               ) : null}
-              {updates.dare && updates.dare > 0 ? (
-                <View style={styles.updates}>
+
+              {updates.new_dare && updates.new_dare > 0 ? (
+                <TouchableOpacity style={styles.updates} onPress={() => handleOpenUpdates('dare')}>
                   <CompassIcon fill={Colors.DARK_BLUE} height={20} width={20} />
-                  <View>
-                    <Text style={styles.updatesTextCount}>+{updates.dare}</Text>
-                    <Text style={styles.updatesText}>new DARE places</Text>
-                  </View>
-                </View>
+                  <Text style={styles.updatesText}>{updates.new_dare} new DAREs</Text>
+                </TouchableOpacity>
               ) : null}
-              {updates.series && updates.series > 0 ? (
-                <View style={styles.updates}>
+
+              {updates.new_series && updates.new_series > 0 ? (
+                <TouchableOpacity
+                  style={styles.updates}
+                  onPress={() => handleOpenUpdates('series')}
+                >
                   <SeriesIcon fill={Colors.DARK_BLUE} height={20} width={20} />
-                  <View>
-                    <Text style={styles.updatesTextCount}>+{updates.series}</Text>
-                    <Text style={styles.updatesText}>new series</Text>
-                  </View>
-                </View>
+                  <Text style={styles.updatesText}>{updates.new_series} new Series</Text>
+                </TouchableOpacity>
               ) : null}
-              {updates.whs && updates.whs > 0 ? (
-                <View style={styles.updates}>
+
+              {updates.new_whs && updates.new_whs > 0 ? (
+                <TouchableOpacity style={styles.updates} onPress={() => handleOpenUpdates('whs')}>
                   <WHSIcon fill={Colors.DARK_BLUE} height={20} width={20} />
-                  <View>
-                    <Text style={styles.updatesTextCount}>+{updates.whs}</Text>
-                    <Text style={styles.updatesText}>new WHS sites</Text>
-                  </View>
-                </View>
-              ) : null}
-              {updates.new_nm && updates.new_nm > 0 ? (
-                <View style={styles.updates}>
-                  <RegionsIcon fill={Colors.DARK_BLUE} height={20} width={20} />
-                  <View>
-                    <Text style={styles.updatesTextCount}>+{updates.new_nm}</Text>
-                    <Text style={styles.updatesText}>new NM regions</Text>
-                  </View>
-                </View>
-              ) : null}
-              {updates.photos && updates.photos > 0 ? (
-                <View style={styles.updates}>
-                  <PhotosIcon fill={Colors.DARK_BLUE} height={20} width={20} />
-                  <View>
-                    <Text style={styles.updatesTextCount}>+{updates.photos}</Text>
-                    <Text style={styles.updatesText}>new photos</Text>
-                  </View>
-                </View>
-              ) : null}
-              {updates.friends && updates.friends > 0 ? (
-                <View style={styles.updates}>
-                  <FriendsIcon fill={Colors.DARK_BLUE} height={20} width={20} />
-                  <View>
-                    <Text style={styles.updatesTextCount}>+{updates.friends}</Text>
-                    <Text style={styles.updatesText}>new friends</Text>
-                  </View>
-                </View>
+                  <Text style={styles.updatesText}>{updates.new_whs} new WHS</Text>
+                </TouchableOpacity>
               ) : null}
             </View>
           </InfoItem>
@@ -459,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}

+ 139 - 32
src/screens/InAppScreens/ProfileScreen/RegionsRenderer/index.tsx

@@ -1,5 +1,5 @@
-import React, { useCallback } from 'react';
-import { View, Text, Image, TouchableOpacity, ScrollView, Dimensions } from 'react-native';
+import React, { useCallback, useEffect, useState } from 'react';
+import { View, Text, Image, TouchableOpacity, ScrollView, Dimensions, Switch } from 'react-native';
 import { Colors } from 'src/theme';
 import { styles } from './styles';
 import { Loading } from 'src/components';
@@ -8,6 +8,8 @@ import CheckSvg from 'assets/icons/mark.svg';
 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 { NAVIGATION_PAGES } from 'src/types';
 
 interface Mega {
   id: number;
@@ -20,6 +22,7 @@ interface Mega {
   }[];
   transits: number[];
   visits: number[];
+  total?: number;
 }
 
 interface Region {
@@ -39,12 +42,68 @@ const RegionsRenderer = ({
   regions: any;
   setIsModalVisible: (value: boolean) => void;
 }) => {
+  const navigation = useNavigation();
   const flashlistConfig = {
     waitForInteraction: true,
     itemVisiblePercentThreshold: 50,
     minimumViewTime: 1000
   };
   const isSmallWidth = Dimensions.get('window').width < 383;
+  const [showAll, setShowAll] = useState(true);
+  const [filteredRegions, setFilteredRegions] = useState<any[]>([]);
+  const [filteredMegaregions, setFilteredMegaregions] = useState<any[]>([]);
+  const disabled = type === 'whs' || type === 'tcc' ? true : false;
+
+  useEffect(() => {
+    if (!regions?.data) return;
+
+    if (showAll) {
+      switch (type) {
+        case 'nm':
+        case 'mqp':
+          setFilteredMegaregions(regions.data.megaregions);
+          break;
+        case 'un':
+        case 'unp':
+        case 'slow':
+        case 'yes':
+        case 'tcc':
+          setFilteredRegions(regions.data[0]);
+          break;
+        case 'whs':
+          setFilteredRegions(regions.data);
+          break;
+      }
+    } else {
+      switch (type) {
+        case 'nm':
+        case 'mqp':
+          const filteredMegaregions = regions.data?.megaregions
+            ?.map((item: Mega) => ({
+              ...item,
+              regions: item.regions.filter((region) => item.visits.includes(region.id)),
+              total: item.regions.length
+            }))
+            ?.filter((item: Mega) => item.regions.length > 0);
+
+          setFilteredMegaregions(filteredMegaregions);
+          break;
+        case 'un':
+        case 'unp':
+        case 'tcc':
+          setFilteredRegions(
+            regions.data[0].filter((item: Region) => regions.data[1].includes(item.name))
+          );
+          break;
+        case 'slow':
+          setFilteredRegions(regions.data[0].filter((item: any) => item.visited === 1));
+          break;
+        case 'whs':
+          setFilteredRegions(regions.data.filter((item: any) => item.visited));
+          break;
+      }
+    }
+  }, [showAll, regions]);
 
   const getOpacity = useCallback(
     (item: any, mega?: Mega) => {
@@ -67,10 +126,51 @@ const RegionsRenderer = ({
     [type, regions?.data]
   );
 
+  const handlePress = (item: any) => {
+    switch (type) {
+      case 'nm':
+      case 'mqp':
+        setIsModalVisible(false);
+        navigation.navigate(
+          ...([
+            NAVIGATION_PAGES.REGION_PREVIEW,
+            {
+              regionId: item.id,
+              type: type === 'nm' ? 'nm' : 'dare',
+              disabled: false,
+              isProfileScreen: true
+            }
+          ] as never)
+        );
+        break;
+      case 'slow':
+      case 'yes':
+      case 'un':
+      case 'unp':
+        setIsModalVisible(false);
+        navigation.navigate(
+          ...([
+            NAVIGATION_PAGES.COUNTRY_PREVIEW,
+            {
+              regionId: type === 'slow' ? item.country_id : item.id,
+              type: 'country',
+              disabled: false,
+              isProfileScreen: true
+            }
+          ] as never)
+        );
+        break;
+    }
+  };
+
   const renderRegion = useCallback(
     (item: any, mega?: Mega) => {
       return (
-        <View style={[styles.regionRow, { opacity: getOpacity(item, mega) }]}>
+        <TouchableOpacity
+          style={[styles.regionRow, { opacity: getOpacity(item, mega) }]}
+          onPress={() => handlePress(item)}
+          disabled={disabled}
+        >
           <View
             style={[
               styles.flags,
@@ -164,7 +264,7 @@ const RegionsRenderer = ({
               </View>
             </View>
           )}
-        </View>
+        </TouchableOpacity>
       );
     },
     [getOpacity, regions?.data, type]
@@ -180,8 +280,8 @@ const RegionsRenderer = ({
               {item.name}
               <Text style={{ fontWeight: '600' }}>
                 {' '}
-                - {item.visits.length}/{item.regions.length} (
-                {((item.visits.length * 100) / item.regions.length).toFixed(2)}%)
+                - {item.visits.length}/{item?.total ?? item.regions.length} (
+                {((item.visits.length * 100) / (item?.total ?? item.regions.length)).toFixed(2)}%)
               </Text>
             </Text>
             {type === 'nm' && (
@@ -231,8 +331,27 @@ const RegionsRenderer = ({
   );
 
   const renderContent = () => {
-    const renderHeader = (headerText: string) => (
-      <RegionsModalHeader textHeader={headerText} onRequestClose={() => setIsModalVisible(false)} />
+    const renderHeader = (headerText: string, rightElement: boolean) => (
+      <RegionsModalHeader
+        textHeader={headerText}
+        onRequestClose={() => setIsModalVisible(false)}
+        rightElement={
+          rightElement ? (
+            <View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
+              <Text style={{ color: Colors.DARK_BLUE, fontSize: 12, fontWeight: '600' }}>
+                Show/Hide All
+              </Text>
+              <Switch
+                trackColor={{ false: Colors.LIGHT_GRAY, true: Colors.DARK_BLUE }}
+                thumbColor={Colors.WHITE}
+                onValueChange={() => setShowAll(!showAll)}
+                value={showAll}
+                style={{ transform: 'scale(0.8)' }}
+              />
+            </View>
+          ) : null
+        }
+      />
     );
 
     switch (type) {
@@ -240,11 +359,11 @@ const RegionsRenderer = ({
       case 'mqp':
         return (
           <>
-            {renderHeader(type === 'nm' ? 'NM' : 'DARE')}
+            {renderHeader(type === 'nm' ? 'NM' : 'DARE', true)}
             <FlashList
               viewabilityConfig={flashlistConfig}
               estimatedItemSize={4000}
-              data={regions.data.megaregions}
+              data={filteredMegaregions}
               renderItem={renderMegaregion}
               keyExtractor={(megaregion) => megaregion?.id?.toString()}
               showsVerticalScrollIndicator={false}
@@ -258,11 +377,11 @@ const RegionsRenderer = ({
       case 'tcc':
         return (
           <>
-            {renderHeader(type === 'un' ? 'UN' : type === 'unp' ? 'UN+' : 'TCC')}
+            {renderHeader(type === 'un' ? 'UN' : type === 'unp' ? 'UN+' : 'TCC', true)}
             <FlashList
               viewabilityConfig={flashlistConfig}
               estimatedItemSize={50}
-              data={regions.data[0]}
+              data={filteredRegions}
               renderItem={(region) => renderRegion(region.item)}
               keyExtractor={(region: Region) => region.name}
               showsVerticalScrollIndicator={false}
@@ -285,11 +404,11 @@ const RegionsRenderer = ({
       case 'slow':
         return (
           <>
-            {renderHeader('SLOW')}
+            {renderHeader('SLOW', true)}
             <FlashList
               viewabilityConfig={flashlistConfig}
               estimatedItemSize={50}
-              data={regions.data[0]}
+              data={filteredRegions}
               renderItem={(region) => renderRegion(region.item)}
               keyExtractor={(region: Region) => region.country_id.toString()}
               showsVerticalScrollIndicator={false}
@@ -301,29 +420,17 @@ const RegionsRenderer = ({
                     <View style={{ flexDirection: 'row', flex: 1 }}>
                       <View style={styles.slow}>
                         <Text style={styles.alignCenter}>
-                          <Text
-                            style={[styles.megaregionTitle, { fontSize: 12 }]}
-                          >
-                            11+
-                          </Text>
+                          <Text style={[styles.megaregionTitle, { fontSize: 12 }]}>11+</Text>
                         </Text>
                       </View>
                       <View style={styles.slow}>
                         <Text style={styles.alignCenter}>
-                          <Text
-                            style={[styles.megaregionTitle, { fontSize: 12 }]}
-                          >
-                            31+
-                          </Text>
+                          <Text style={[styles.megaregionTitle, { fontSize: 12 }]}>31+</Text>
                         </Text>
                       </View>
                       <View style={styles.slow}>
                         <Text style={styles.alignCenter}>
-                          <Text
-                            style={[styles.megaregionTitle, { fontSize: 12 }]}
-                          >
-                            101+
-                          </Text>
+                          <Text style={[styles.megaregionTitle, { fontSize: 12 }]}>101+</Text>
                         </Text>
                       </View>
                     </View>
@@ -348,7 +455,7 @@ const RegionsRenderer = ({
         };
         return (
           <>
-            {renderHeader('YES')}
+            {renderHeader('YES', false)}
             <FlashList
               viewabilityConfig={flashlistConfig}
               estimatedItemSize={50}
@@ -390,11 +497,11 @@ const RegionsRenderer = ({
       case 'whs':
         return (
           <>
-            {renderHeader('WHS')}
+            {renderHeader('WHS', true)}
             <FlashList
               viewabilityConfig={flashlistConfig}
               estimatedItemSize={50}
-              data={regions.data}
+              data={filteredRegions}
               renderItem={(region) => renderRegion(region.item)}
               keyExtractor={(region: Region) => region.name}
               showsVerticalScrollIndicator={false}

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

@@ -0,0 +1,214 @@
+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);
+    if (type === 'nm' || type === 'dare') {
+      navigation.navigate(
+        ...([
+          NAVIGATION_PAGES.REGION_PREVIEW,
+          {
+            regionId: item.id,
+            type: type === 'nm' ? 'nm' : 'dare',
+            disabled: false,
+            isProfileScreen: true
+          }
+        ] as never)
+      );
+    } else {
+      navigation.navigate(
+        ...([
+          NAVIGATION_PAGES.COUNTRY_PREVIEW,
+          {
+            regionId: item.id,
+            type: 'country',
+            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, index) => item.id.toString() + index.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 }) => (
+                <TouchableOpacity onPress={() => handlePress(item)} style={styles.item}>
+                  <Image
+                    source={{
+                      uri: API_HOST + '/img/flags_new/' + item.flag
+                    }}
+                    style={styles.regionsFlag}
+                  />
+                  <Text style={styles.regionName}>{item.country}</Text>
+                </TouchableOpacity>
+              )}
+              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
+  }
+});

+ 2 - 3
src/screens/InAppScreens/ProfileScreen/index.tsx

@@ -4,7 +4,6 @@ import { CommonActions, NavigationProp, useFocusEffect } from '@react-navigation
 import ReactModal from 'react-native-modal';
 
 import { usePostGetProfileInfoDataQuery, usePostGetProfileUpdatesQuery } from '@api/user';
-
 import {
   PageWrapper,
   Loading,
@@ -100,7 +99,7 @@ const ProfileScreen: FC<Props> = ({ navigation, route }) => {
     }
   }, [userData]);
 
-  if (!userData?.data || !lastUpdates?.data || isFetching) return <Loading />;
+  if (!userData?.data || !lastUpdates || isFetching) return <Loading />;
 
   const data = userData.data;
   const links = JSON.parse(data.user_data.links_json);
@@ -325,7 +324,7 @@ const ProfileScreen: FC<Props> = ({ navigation, route }) => {
             friendDbId: data.friend_db_id,
             ownProfile: data.own_profile
           }}
-          updates={lastUpdates.data.updates}
+          updates={lastUpdates?.data ? lastUpdates.data?.updates : null}
           userId={isPublicView ? route.params?.userId : +currentUserId}
           navigation={navigation}
           isPublicView={isPublicView}

+ 3 - 3
src/screens/InAppScreens/TravellersScreen/SeriesRankingListScreen/index.tsx

@@ -20,9 +20,9 @@ import { Colors } from 'src/theme';
 import { SeriesRanking } from '../utils/types';
 
 const SeriesRankingListScreen = ({ route }: { route: any }) => {
-  const name = route.params.name;
-  const id = route.params.id;
-  const series = route.params.series;
+  const name = route.params?.name;
+  const id = route.params?.id;
+  const series = route.params?.series;
   const [index, setIndex] = useState(0);
   const [routes, setRoutes] = useState<{ key: string; title: string }[]>([]);
   const [loading, setLoading] = useState(true);

+ 338 - 0
src/screens/InAppScreens/TravelsScreen/AddNewFixerScreen/index.tsx

@@ -0,0 +1,338 @@
+import React, { useEffect, useState } from 'react';
+import {
+  View,
+  Text,
+  TouchableOpacity,
+  ScrollView,
+  Image,
+  Platform,
+  KeyboardAvoidingView
+} from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+import { Dropdown, MultiSelect } from 'react-native-searchable-dropdown-kj';
+import { Formik } from 'formik';
+import * as yup from 'yup';
+
+import { PageWrapper, Header, Input, CheckBox } from 'src/components';
+import { StoreType, storage } from 'src/storage';
+import { Colors } from 'src/theme';
+import { NAVIGATION_PAGES } from 'src/types';
+import { styles } from './styles';
+import {
+  useGetAllCountriesQuery,
+  usePostAddFixerMutation,
+  usePostEditFixerMutation
+} from '@api/fixers';
+import { API_HOST } from 'src/constants';
+import { months } from '../utils/constants';
+import { FixerType } from '../utils/types';
+
+import CheckSvg from 'assets/icons/mark.svg';
+import CrossSvg from 'assets/icons/close.svg';
+
+const NewFixerSchema = yup.object().shape({
+  selectedCountries: yup
+    .array()
+    .min(1, 'select at least one country')
+    .required('country is required'),
+  name: yup.string().required('name is required'),
+  email: yup.string().email('invalid email format'),
+  website: yup.string(),
+  comment: yup
+    .string()
+    .required('comment is required')
+    .max(8000, 'comment should not exceed 8000 characters')
+});
+
+const AddNewFixerScreen = ({ route }: { route: any }) => {
+  const existingFixer = route.params?.fixer ?? null;
+  const token = storage.get('token', StoreType.STRING) as string;
+  const navigation = useNavigation();
+  const { data: countries } = useGetAllCountriesQuery(token, true);
+  const { mutateAsync: addFixer } = usePostAddFixerMutation();
+  const { mutateAsync: editFixer } = usePostEditFixerMutation();
+  const [allCountries, setAllCountries] = useState<
+    { label: string; value: number; flag: string }[]
+  >([]);
+
+  useEffect(() => {
+    if (countries?.data) {
+      setAllCountries([
+        ...countries.data.map((item) => ({
+          label: item.country,
+          value: item.id,
+          flag: item.flag
+        }))
+      ]);
+    }
+  }, [countries]);
+
+  const years = Array.from({ length: 75 }, (_, i) => {
+    const year = new Date().getFullYear() - i;
+
+    return { label: year.toString(), value: year };
+  });
+
+  const month = new Date().getMonth() + 1;
+  const year = new Date().getFullYear();
+
+  const initialData: FixerType = existingFixer
+    ? {
+        month: existingFixer.month,
+        year: existingFixer.year,
+        selectedCountries: [route.params?.un_id],
+        name: existingFixer.name,
+        email: existingFixer.email,
+        phone: existingFixer.phone,
+        website: existingFixer.web,
+        comment: existingFixer.comment,
+        anonymous: existingFixer.anonymous === 1
+      }
+    : {
+        month: month,
+        year: year,
+        selectedCountries: [],
+        name: '',
+        email: '',
+        phone: '',
+        website: '',
+        comment: '',
+        anonymous: false
+      };
+
+  return (
+    <Formik
+      initialValues={initialData}
+      validationSchema={NewFixerSchema}
+      onSubmit={async (values) => {
+        if (existingFixer) {
+          await editFixer(
+            {
+              token,
+              fixer_id: existingFixer.id,
+              month: values.month,
+              year: values.year,
+              un_ids: values.selectedCountries,
+              name: values.name,
+              anonymous: values.anonymous ? 1 : 0,
+              email: values.email,
+              phone: values.phone,
+              website: values.website,
+              comment: values.comment
+            },
+            {
+              onSuccess: () => {
+                navigation.navigate(...([NAVIGATION_PAGES.FIXERS, { saved: true }] as never));
+              }
+            }
+          );
+        } else {
+          await addFixer(
+            {
+              token,
+              month: values.month,
+              year: values.year,
+              un_ids: values.selectedCountries,
+              name: values.name,
+              anonymous: values.anonymous ? 1 : 0,
+              email: values.email,
+              phone: values.phone,
+              website: values.website,
+              comment: values.comment
+            },
+            {
+              onSuccess: () => {
+                navigation.navigate(...([NAVIGATION_PAGES.FIXERS, { saved: true }] as never));
+              }
+            }
+          );
+        }
+      }}
+    >
+      {({ values, errors, touched, handleChange, handleBlur, handleSubmit, setFieldValue }) => (
+        <PageWrapper>
+          <Header label={existingFixer ? 'Edit Fixer' : 'Add New Fixer'} />
+          <KeyboardAvoidingView
+            behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
+            style={{ flex: 1 }}
+          >
+            <ScrollView
+              contentContainerStyle={styles.scrollContainer}
+              showsVerticalScrollIndicator={false}
+            >
+              <View>
+                <Text style={styles.title}>Date</Text>
+                <View style={[styles.row, { justifyContent: 'space-between' }]}>
+                  <Dropdown
+                    style={styles.dateSelector}
+                    placeholderStyle={styles.placeholderStyle}
+                    selectedTextStyle={styles.placeholderStyle}
+                    containerStyle={styles.dropdownContent}
+                    data={months}
+                    labelField="label"
+                    valueField="value"
+                    value={values.month}
+                    placeholder="Month"
+                    onChange={(item) => setFieldValue('month', item.value)}
+                    autoScroll={false}
+                    flatListProps={{ initialNumToRender: 50, maxToRenderPerBatch: 10 }}
+                  />
+                  <Dropdown
+                    style={styles.dateSelector}
+                    placeholderStyle={styles.placeholderStyle}
+                    selectedTextStyle={styles.placeholderStyle}
+                    containerStyle={styles.dropdownContent}
+                    data={years}
+                    labelField="label"
+                    valueField="value"
+                    value={values.year}
+                    placeholder="Year"
+                    onChange={(item) => setFieldValue('year', item.value)}
+                    search={true}
+                    searchPlaceholder="Search"
+                    autoScroll={false}
+                    inputSearchStyle={styles.search}
+                    flatListProps={{ initialNumToRender: 50, maxToRenderPerBatch: 10 }}
+                    searchQuery={(keyword, item) => item.includes(keyword)}
+                  />
+                </View>
+              </View>
+
+              <View style={{ flex: 1 }}>
+                <Text style={styles.title}>Countries</Text>
+                <MultiSelect
+                  style={[
+                    styles.dateSelector,
+                    {
+                      width: '100%',
+                      marginBottom: values.selectedCountries.length ? 4 : 0
+                    },
+                    touched.selectedCountries && errors.selectedCountries
+                      ? { borderColor: Colors.RED, borderWidth: 1 }
+                      : {}
+                  ]}
+                  placeholderStyle={styles.placeholderStyle}
+                  selectedTextStyle={styles.placeholderStyle}
+                  containerStyle={styles.dropdownContent}
+                  data={allCountries}
+                  labelField="label"
+                  valueField="value"
+                  value={values.selectedCountries}
+                  placeholder="Select countries"
+                  activeColor="#E7E7E7"
+                  search={true}
+                  searchPlaceholder="Search"
+                  inputSearchStyle={styles.search}
+                  searchQuery={(keyword, item) =>
+                    item.toLowerCase().includes(keyword.toLowerCase())
+                  }
+                  flatListProps={{ initialNumToRender: 30, maxToRenderPerBatch: 10 }}
+                  onChange={(item) => setFieldValue('selectedCountries', item)}
+                  renderItem={(item) => (
+                    <View style={[styles.row, styles.multiOption]}>
+                      <View style={[styles.row, { gap: 8, flex: 1 }]}>
+                        <Image
+                          source={{ uri: API_HOST + item.flag }}
+                          style={[styles.flag, styles.borderSolid]}
+                        />
+                        <Text style={styles.optionText}>{item.label}</Text>
+                      </View>
+
+                      {values.selectedCountries.includes(item.value) && (
+                        <CheckSvg fill={Colors.DARK_BLUE} height={8} />
+                      )}
+                    </View>
+                  )}
+                  renderSelectedItem={(item, unSelect) => (
+                    <TouchableOpacity style={[styles.row, styles.countryItem]} onPress={unSelect}>
+                      <Image
+                        source={{ uri: API_HOST + item.flag }}
+                        style={[styles.flagSmall, styles.borderSolid]}
+                      />
+                      <Text style={styles.label}>{item.label}</Text>
+                      <CrossSvg fill={Colors.DARK_BLUE} height={10} />
+                    </TouchableOpacity>
+                  )}
+                />
+                {touched.selectedCountries && errors.selectedCountries && (
+                  <Text style={styles.textError}>{errors.selectedCountries}</Text>
+                )}
+              </View>
+
+              <Input
+                placeholder="Name"
+                inputMode={'text'}
+                onChange={handleChange('name')}
+                onBlur={handleBlur('name')}
+                value={values.name}
+                header="Name"
+                formikError={touched.name && errors.name}
+              />
+
+              <Input
+                placeholder="E-mail"
+                inputMode={'email'}
+                onChange={handleChange('email')}
+                onBlur={handleBlur('email')}
+                value={values.email}
+                header="E-mail"
+                formikError={touched.email && errors.email}
+              />
+
+              <Input
+                placeholder="Phone"
+                inputMode={'tel'}
+                onChange={handleChange('phone')}
+                onBlur={handleBlur('phone')}
+                value={values.phone}
+                header="Phone"
+              />
+
+              <Input
+                placeholder="Website"
+                inputMode={'url'}
+                onChange={handleChange('website')}
+                onBlur={handleBlur('website')}
+                value={values.website}
+                header="Website"
+                formikError={touched.website && errors.website}
+              />
+
+              <Input
+                placeholder="Comment"
+                inputMode={'text'}
+                onChange={handleChange('comment')}
+                onBlur={handleBlur('comment')}
+                value={values.comment}
+                header="Comment"
+                multiline={true}
+                formikError={touched.comment && errors.comment}
+              />
+
+              <TouchableOpacity
+                onPress={() => setFieldValue('anonymous', !values.anonymous)}
+                style={[styles.row, { gap: 8 }]}
+              >
+                <CheckBox
+                  onChange={(value) => setFieldValue('anonymous', value)}
+                  value={values.anonymous}
+                />
+                <Text style={[styles.title, { marginBottom: 0 }]}>
+                  share this information anonymously
+                </Text>
+              </TouchableOpacity>
+            </ScrollView>
+          </KeyboardAvoidingView>
+
+          <View style={[styles.tabContainer, styles.row]}>
+            <TouchableOpacity style={styles.tabStyle} onPress={() => handleSubmit()}>
+              <Text style={styles.tabText}>{existingFixer ? 'Save Fixer' : 'Add New Fixer'}</Text>
+            </TouchableOpacity>
+          </View>
+        </PageWrapper>
+      )}
+    </Formik>
+  );
+};
+
+export default AddNewFixerScreen;

+ 91 - 0
src/screens/InAppScreens/TravelsScreen/AddNewFixerScreen/styles.tsx

@@ -0,0 +1,91 @@
+import { StyleSheet } from 'react-native';
+import { Colors } from 'src/theme';
+import { getFontSize } from 'src/utils';
+
+export const styles = StyleSheet.create({
+  scrollContainer: { flexGrow: 1, gap: 16, paddingBottom: 16 },
+  tabContainer: {
+    gap: 16,
+    marginVertical: 8
+  },
+  tabStyle: {
+    flex: 1,
+    borderRadius: 4,
+    alignItems: 'center',
+    justifyContent: 'center',
+    paddingHorizontal: 16,
+    paddingVertical: 12,
+    gap: 4,
+    borderWidth: 1,
+    backgroundColor: Colors.ORANGE,
+    borderColor: Colors.ORANGE
+  },
+  tabText: {
+    fontSize: getFontSize(14),
+    fontWeight: 'bold',
+    fontFamily: 'redhat-700',
+    color: Colors.WHITE
+  },
+  row: { flexDirection: 'row', alignItems: 'center' },
+  multiOption: {
+    paddingHorizontal: 12,
+    paddingVertical: 8,
+    justifyContent: 'space-between'
+  },
+  optionText: { fontSize: 16, color: Colors.DARK_BLUE, flex: 1 },
+  textError: {
+    color: Colors.RED,
+    fontSize: getFontSize(12),
+    fontFamily: 'redhat-600',
+    marginTop: 5
+  },
+  title: {
+    color: Colors.DARK_BLUE,
+    fontSize: getFontSize(14),
+    fontFamily: 'redhat-700',
+    marginBottom: 5
+  },
+  dateSelector: {
+    width: '47%',
+    height: 44,
+    backgroundColor: '#F4F4F4',
+    borderRadius: 4,
+    paddingHorizontal: 8
+  },
+  placeholderStyle: {
+    fontSize: 16,
+    color: Colors.DARK_BLUE
+  },
+  dropdownContent: {
+    borderRadius: 4
+  },
+  search: {
+    height: 40,
+    borderRadius: 4
+  },
+  countryItem: {
+    gap: 6,
+    paddingHorizontal: 10,
+    paddingVertical: 8,
+    marginRight: 8,
+    marginTop: 8,
+    borderRadius: 4,
+    borderWidth: 0.5,
+    borderColor: Colors.DARK_BLUE
+  },
+  flagSmall: {
+    borderRadius: 10,
+    width: 20,
+    height: 20
+  },
+  flag: {
+    borderRadius: 12,
+    width: 24,
+    height: 24
+  },
+  label: { fontSize: 12, fontWeight: '600', color: Colors.DARK_BLUE },
+  borderSolid: {
+    borderColor: Colors.FILL_LIGHT,
+    borderWidth: 1
+  }
+});

+ 6 - 2
src/screens/InAppScreens/TravelsScreen/AddPhotoScreen/index.tsx

@@ -191,7 +191,7 @@ const AddPhotoScreen = ({ route }: { route: any }) => {
     };
     saveTemp(tempData, {
       onSuccess: () => {
-        data ? navigation.pop(2) : navigation.goBack();
+        data && allRegions?.length ? navigation.pop(2) : navigation.goBack();
       }
     });
   };
@@ -239,7 +239,11 @@ const AddPhotoScreen = ({ route }: { route: any }) => {
             title="Save"
             onPress={saveTempData}
             icon={<SaveSvg fill={Colors.DARK_BLUE} />}
-            disabled={!selectedRegion || imagesStatus.length === 0}
+            disabled={
+              !selectedRegion ||
+              imagesStatus.length === 0 ||
+              imagesStatus.some((img) => !img.loaded)
+            }
           />
           <CustomButton
             title="Add photo"

+ 1 - 1
src/screens/InAppScreens/TravelsScreen/AddRegionsScreen/index.tsx

@@ -136,7 +136,7 @@ const AddRegionsScreen = ({ route }: { route: any }) => {
       const { latitude, longitude } = event.nativeEvent.coordinate;
       const point = turf.point([longitude, latitude]);
 
-      let foundRegion = findRegionInDataset(regionsGeojson, point);
+      let foundRegion = regionsGeojson ? findRegionInDataset(regionsGeojson, point) : null;
 
       if (foundRegion) {
         const id = foundRegion.properties?.id;

+ 1 - 0
src/screens/InAppScreens/TravelsScreen/Components/CountryItem/index.tsx

@@ -37,6 +37,7 @@ const CountryItem = React.memo(
         onPress={() => {
           setUserData({
             type: 'countries',
+            id: item.country_id,
             visited: Boolean(item.visited),
             slow11: item.slow11,
             slow31: item.slow31,

+ 225 - 0
src/screens/InAppScreens/TravelsScreen/Components/FixerItem/index.tsx

@@ -0,0 +1,225 @@
+import React, { Dispatch, SetStateAction, useState } from 'react';
+import { View, Text, TouchableOpacity, Image, Linking } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+import moment from 'moment';
+
+import { API_HOST } from 'src/constants';
+import { Colors } from 'src/theme';
+import { NAVIGATION_PAGES } from 'src/types';
+import { styles } from './styles';
+import { FixersData } from '../../utils/types';
+import { Star } from '../Star';
+
+interface FixerItemProps {
+  item: FixersData;
+  setSelectedFixer: Dispatch<SetStateAction<FixersData | null>>;
+  country: { id: number; country: string; flag: string };
+  setIsWarningModalVisible: Dispatch<SetStateAction<boolean>>;
+}
+
+const FixerItem = ({
+  item,
+  setSelectedFixer,
+  country,
+  setIsWarningModalVisible
+}: FixerItemProps) => {
+  const navigation = useNavigation();
+
+  const formatDate = (month: number) => {
+    const formatedMonth = moment(month, 'MM').format('MMMM');
+    return `${item.year} ${formatedMonth}`;
+  };
+
+  const renderRatingStars = (ratings: { rate: string; name: string; comment: string }[]) => {
+    const total = ratings.reduce((sum, rating) => sum + parseFloat(rating.rate), 0);
+    const rating = (total / ratings.length).toFixed(4);
+    const stars = [];
+
+    for (let i = 0; i < 5; i++) {
+      const filled = Math.min(Math.max(+rating - i, 0), 1);
+      stars.push(<Star key={i} filled={filled} />);
+    }
+    return (
+      <TouchableOpacity
+        style={styles.ratingContainer}
+        onPress={() =>
+          navigation.navigate(
+            ...([
+              NAVIGATION_PAGES.FIXERS_COMMENTS,
+              { comments: item.ratings, name: item.name }
+            ] as never)
+          )
+        }
+      >
+        {stars}
+        <Text style={styles.labelText}>({item.ratings.length})</Text>
+      </TouchableOpacity>
+    );
+  };
+
+  const openURLWithPrefix = async (url: string) => {
+    const prefixedURL = url.startsWith('http://') || url.startsWith('https://') ? url : `http://${url}`;
+    const supported = await Linking.canOpenURL(prefixedURL);
+  
+    if (supported) {
+      Linking.openURL(prefixedURL);
+    }
+  };
+
+  return (
+    <View style={styles.fixerItemContainer}>
+      <View style={styles.fixerHeaderContainer}>
+        <View style={styles.fixerCountryContainer}>
+          <Image source={{ uri: API_HOST + country.flag }} style={styles.flagIcon} />
+          <Text style={styles.fixerCountryText}>{country.country}</Text>
+        </View>
+      </View>
+
+      <View style={styles.divider} />
+
+      <View style={styles.detailContainer}>
+        {item.name && (
+          <View style={styles.rowContent}>
+            <Text style={styles.labelText}>Name:</Text>
+            <Text style={styles.valueText}>{item.name}</Text>
+          </View>
+        )}
+
+        <View style={styles.rowContent}>
+          <Text style={styles.labelText}>Date:</Text>
+          <Text style={[styles.valueText, { color: Colors.DARK_BLUE }]}>
+            {formatDate(item.month)}
+          </Text>
+        </View>
+
+        {item.email && (
+          <View style={styles.rowContent}>
+            <Text style={styles.labelText}>Email:</Text>
+            <Text
+              style={styles.valueText}
+              onPress={() => Linking.openURL(`mailto:${item.email}`)}
+              selectable
+            >
+              {item.email}
+            </Text>
+          </View>
+        )}
+
+        {item.phone && (
+          <View style={styles.rowContent}>
+            <Text style={styles.labelText}>Phone:</Text>
+            <Text
+              style={styles.valueText}
+              onPress={() => {
+                Linking.openURL(`tel:${item.phone}`);
+              }}
+              selectable
+            >
+              {item.phone}
+            </Text>
+          </View>
+        )}
+
+        {item.web && (
+          <View style={styles.rowContent}>
+            <Text style={styles.labelText}>Website:</Text>
+            <TouchableOpacity style={{ flex: 4 }} onPress={() => openURLWithPrefix(item.web)}>
+              <Text style={[styles.linkText]} selectable>{item.web}</Text>
+            </TouchableOpacity>
+          </View>
+        )}
+
+        {item.comment && <CommentText text={item.comment} />}
+
+        {item.ratings.length ? (
+          <View style={styles.rowContent}>
+            <Text style={styles.labelText}>Rating:</Text>
+            {renderRatingStars(item.ratings)}
+          </View>
+        ) : null}
+
+        <View style={styles.rowContent}>
+          <Text style={styles.labelText}>Added by:</Text>
+          <TouchableOpacity
+            onPress={() =>
+              navigation.navigate(
+                ...([NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW, { userId: item.added_by_uid }] as never)
+              )
+            }
+            disabled={item.added_by_uid === 0}
+            style={{ flex: 4 }}
+          >
+            <Text style={[styles.valueText, { color: Colors.DARK_BLUE, flex: 0 }]}>
+              {item.added_by_name}
+            </Text>
+          </TouchableOpacity>
+        </View>
+      </View>
+
+      <View style={styles.divider} />
+
+      <View style={{ justifyContent: 'flex-end', flexDirection: 'row', gap: 12 }}>
+        <TouchableOpacity
+          style={styles.rateButton}
+          onPress={() => {
+            if (item.can_rate) {
+              setSelectedFixer(item);
+            } else {
+              setIsWarningModalVisible(true);
+            }
+          }}
+        >
+          <Text style={styles.rateButtonText}>Rate</Text>
+        </TouchableOpacity>
+
+        {item.can_edit ? (
+          <TouchableOpacity
+            style={styles.rateButton}
+            onPress={() => {
+              navigation.navigate(
+                ...([NAVIGATION_PAGES.ADD_FIXER, { fixer: item, un_id: country.id }] as never)
+              );
+            }}
+          >
+            <Text style={styles.rateButtonText}>Edit</Text>
+          </TouchableOpacity>
+        ) : null}
+      </View>
+    </View>
+  );
+};
+
+const CommentText = ({ text }: { text: string }) => {
+  const [showFullComment, setShowFullComment] = useState(false);
+  const [textMoreThanThreeLines, setTextMoreThanThreeLines] = useState(false);
+
+  return (
+    <View style={{ gap: 6 }}>
+      <Text style={styles.labelText}>Comment:</Text>
+      <View>
+        <Text
+          style={[styles.valueText, { color: Colors.DARK_BLUE }]}
+          numberOfLines={textMoreThanThreeLines && !showFullComment ? 3 : undefined}
+          onTextLayout={(e) => {
+            if (!textMoreThanThreeLines) {
+              const { lines } = e.nativeEvent;
+              if (lines.length > 3) {
+                setTextMoreThanThreeLines(true);
+              }
+            }
+          }}
+        >
+          {text}
+        </Text>
+
+        {textMoreThanThreeLines && (
+          <TouchableOpacity onPress={() => setShowFullComment(!showFullComment)}>
+            <Text style={styles.linkText}>{showFullComment ? 'Show less' : 'Show more'}</Text>
+          </TouchableOpacity>
+        )}
+      </View>
+    </View>
+  );
+};
+
+export default FixerItem;

+ 86 - 0
src/screens/InAppScreens/TravelsScreen/Components/FixerItem/styles.tsx

@@ -0,0 +1,86 @@
+import { StyleSheet } from 'react-native';
+import { Colors } from 'src/theme';
+
+export const styles = StyleSheet.create({
+  fixerItemContainer: {
+    paddingHorizontal: 16,
+    paddingTop: 12,
+    paddingBottom: 12,
+    marginVertical: 8,
+    backgroundColor: Colors.FILL_LIGHT,
+    borderRadius: 8,
+    gap: 12
+  },
+  fixerHeaderContainer: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center'
+  },
+  fixerCountryContainer: {
+    flexDirection: 'row',
+    gap: 8,
+    alignItems: 'center'
+  },
+  fixerCountryText: {
+    fontSize: 14,
+    fontWeight: '700',
+    color: Colors.DARK_BLUE
+  },
+  divider: {
+    height: 1,
+    backgroundColor: Colors.DARK_LIGHT
+  },
+  flagIcon: {
+    width: 24,
+    height: 24,
+    resizeMode: 'cover',
+    borderWidth: 0.5,
+    borderRadius: 12,
+    borderColor: '#B4C2C7'
+  },
+  rateButton: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    borderRadius: 4,
+    paddingVertical: 10,
+    paddingHorizontal: 22,
+    gap: 6,
+    backgroundColor: Colors.ORANGE
+  },
+  rateButtonText: {
+    fontSize: 13,
+    color: Colors.WHITE,
+    fontWeight: '700'
+  },
+  detailContainer: {
+    gap: 16
+  },
+  labelText: {
+    fontSize: 12,
+    fontWeight: '600',
+    color: Colors.DARK_BLUE,
+    flex: 1
+  },
+  valueText: {
+    fontSize: 12,
+    color: Colors.ORANGE,
+    fontWeight: '700',
+    flex: 4
+  },
+  linkText: {
+    fontSize: 12,
+    color: Colors.ORANGE,
+    fontWeight: '700'
+  },
+  ratingContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    gap: 2,
+    flex: 4
+  },
+  rowContent: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    gap: 16
+  }
+});

+ 151 - 0
src/screens/InAppScreens/TravelsScreen/Components/RateModal/index.tsx

@@ -0,0 +1,151 @@
+import React, { Dispatch, SetStateAction } from 'react';
+import Modal from 'react-native-modal';
+import {
+  Text,
+  TouchableOpacity,
+  View,
+  KeyboardAvoidingView,
+  ScrollView,
+  Platform
+} from 'react-native';
+import { Formik } from 'formik';
+import * as yup from 'yup';
+
+import { ModalStyles } from './styles';
+import { Colors } from 'src/theme';
+import { Button, Input } from 'src/components';
+import { ButtonVariants } from 'src/types/components';
+import StarRating from '../StarRating';
+import { FixersData } from '../../utils/types';
+
+import CloseIcon from 'assets/icons/close.svg';
+
+const validationSchema = yup.object().shape({
+  rating1: yup.number().required('required').min(1, 'field is required'),
+  rating2: yup.number().required('required').min(1, 'field is required'),
+  rating3: yup.number().required('required').min(1, 'field is required'),
+  comment: yup
+    .string()
+    .required('comment is required')
+    .min(5, 'comment should be at least 5 characters')
+    .max(8000, 'comment should not exceed 8000 characters')
+});
+
+interface RateModalProps {
+  selectedFixer: FixersData;
+  setSelectedFixer: Dispatch<SetStateAction<FixersData | null>>;
+  saveRating: (values: {
+    rating1: number;
+    rating2: number;
+    rating3: number;
+    comment: string;
+  }) => void;
+}
+
+export const RateModal = ({ selectedFixer, setSelectedFixer, saveRating }: RateModalProps) => {
+  return (
+    <Modal isVisible={selectedFixer ? true : false}>
+      <KeyboardAvoidingView
+        behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
+        style={{ flex: 1, justifyContent: 'center' }}
+      >
+        <ScrollView contentContainerStyle={{ flexGrow: 1, justifyContent: 'center' }}>
+          <View style={ModalStyles.modal}>
+            <View style={ModalStyles.modalContent}>
+              <View style={{ alignSelf: 'flex-end' }}>
+                <TouchableOpacity onPress={() => setSelectedFixer(null)}>
+                  <CloseIcon />
+                </TouchableOpacity>
+              </View>
+              <Formik
+                initialValues={{
+                  rating1: 0,
+                  rating2: 0,
+                  rating3: 0,
+                  comment: ''
+                }}
+                validationSchema={validationSchema}
+                onSubmit={(values) => {
+                  saveRating(values);
+                }}
+              >
+                {({ handleChange, handleSubmit, setFieldValue, values, errors, touched }) => (
+                  <View style={{ display: 'flex', gap: 16 }}>
+                    <Text style={ModalStyles.title}>Rate fixer {selectedFixer?.name}</Text>
+
+                    <View style={ModalStyles.rateItem}>
+                      <Text style={ModalStyles.header}>Responsiveness (digital communication)</Text>
+                      <StarRating
+                        rating={values.rating1}
+                        onRatingChange={(rating: number) => setFieldValue('rating1', rating)}
+                      />
+                      {errors.rating1 && touched.rating1 && (
+                        <Text style={ModalStyles.textError}>{errors.rating1}</Text>
+                      )}
+                    </View>
+
+                    <View style={ModalStyles.rateItem}>
+                      <Text style={ModalStyles.header}>Communication (language skills)</Text>
+                      <StarRating
+                        rating={values.rating2}
+                        onRatingChange={(rating: number) => setFieldValue('rating2', rating)}
+                      />
+                      {errors.rating2 && touched.rating2 && (
+                        <Text style={ModalStyles.textError}>{errors.rating2}</Text>
+                      )}
+                    </View>
+
+                    <View style={ModalStyles.rateItem}>
+                      <Text style={ModalStyles.header}>On site support</Text>
+                      <StarRating
+                        rating={values.rating3}
+                        onRatingChange={(rating: number) => setFieldValue('rating3', rating)}
+                      />
+                      {errors.rating3 && touched.rating3 && (
+                        <Text style={ModalStyles.textError}>{errors.rating3}</Text>
+                      )}
+                    </View>
+
+                    <View style={ModalStyles.rateItem}>
+                      <Text style={ModalStyles.header}>Comment</Text>
+                      <Input
+                        onChange={handleChange('comment')}
+                        value={values.comment}
+                        placeholder={'Your comment...'}
+                        multiline
+                        inputMode={'text'}
+                        height={64}
+                        formikError={errors.comment && touched.comment ? errors.comment : ''}
+                      />
+                    </View>
+
+                    <View style={ModalStyles.buttonsWrapper}>
+                      <Button
+                        variant={ButtonVariants.OPACITY}
+                        containerStyles={ModalStyles.buttonClose}
+                        textStyles={{
+                          color: Colors.DARK_BLUE
+                        }}
+                        onPress={() => setSelectedFixer(null)}
+                        children={'Close'}
+                      />
+                      <Button
+                        variant={ButtonVariants.FILL}
+                        containerStyles={ModalStyles.buttonSave}
+                        textStyles={{
+                          color: Colors.WHITE
+                        }}
+                        onPress={handleSubmit}
+                        children={'Save'}
+                      />
+                    </View>
+                  </View>
+                )}
+              </Formik>
+            </View>
+          </View>
+        </ScrollView>
+      </KeyboardAvoidingView>
+    </Modal>
+  );
+};

+ 48 - 0
src/screens/InAppScreens/TravelsScreen/Components/RateModal/styles.tsx

@@ -0,0 +1,48 @@
+import { Colors } from 'src/theme';
+import { StyleSheet } from 'react-native';
+import { getFontSize } from 'src/utils';
+
+export const ModalStyles = StyleSheet.create({
+  buttonsWrapper: {
+    width: '100%',
+    display: 'flex',
+    justifyContent: 'space-between',
+    flexDirection: 'row',
+    marginTop: 20
+  },
+  textError: {
+    fontSize: getFontSize(12),
+    fontWeight: '500',
+    color: '#EF5B5B'
+  },
+  header: {
+    color: Colors.DARK_BLUE,
+    fontSize: getFontSize(12),
+    fontFamily: 'redhat-600'
+  },
+  modal: { backgroundColor: 'white', borderRadius: 15 },
+  modalContent: {
+    marginLeft: '5%',
+    marginRight: '5%',
+    marginTop: '5%',
+    marginBottom: '10%'
+  },
+  title: {
+    color: Colors.DARK_BLUE,
+    fontSize: 18,
+    fontWeight: '700',
+    textAlign: 'center',
+    marginBottom: 8
+  },
+  rateItem: { alignItems: 'flex-start', gap: 8 },
+  buttonClose: {
+    borderColor: Colors.DARK_BLUE,
+    backgroundColor: Colors.WHITE,
+    width: '45%'
+  },
+  buttonSave: {
+    borderColor: Colors.DARK_BLUE,
+    backgroundColor: Colors.DARK_BLUE,
+    width: '45%'
+  }
+});

+ 29 - 0
src/screens/InAppScreens/TravelsScreen/Components/Star/index.tsx

@@ -0,0 +1,29 @@
+import { View, StyleSheet, TouchableOpacity } from 'react-native';
+import { FontAwesome } from '@expo/vector-icons';
+import { Colors } from 'src/theme';
+
+export const Star = ({ filled }: { filled: number }) => {
+  return (
+    <View style={styles.container}>
+      <FontAwesome name="star-o" size={15} color={Colors.DARK_BLUE} />
+      <View style={[styles.star, { width: `${filled * 100}%` }]}>
+        <FontAwesome name="star" size={15} color={Colors.DARK_BLUE} />
+      </View>
+    </View>
+  );
+};
+
+const styles = StyleSheet.create({
+  container: {
+    position: 'relative',
+    width: 15,
+    height: 15
+  },
+  star: {
+    position: 'absolute',
+    top: 0,
+    left: 0,
+    height: '100%',
+    overflow: 'hidden'
+  }
+});

+ 71 - 0
src/screens/InAppScreens/TravelsScreen/Components/StarRating/index.tsx

@@ -0,0 +1,71 @@
+import React from 'react';
+import { View, TouchableOpacity } from 'react-native';
+import Animated, {
+  useSharedValue,
+  useAnimatedStyle,
+  withTiming,
+  interpolateColor
+} from 'react-native-reanimated';
+import { FontAwesome } from '@expo/vector-icons';
+
+import { Colors } from 'src/theme';
+
+const StarRating = ({
+  rating,
+  onRatingChange
+}: {
+  rating: number;
+  onRatingChange: (s: number) => void;
+}) => {
+  const animatedValues = Array(5)
+    .fill(0)
+    .map(() => useSharedValue(1));
+  const colorValues = Array(5)
+    .fill(0)
+    .map(() => useSharedValue(0));
+
+  const handlePress = (star: number) => {
+    onRatingChange(star);
+    animateStar(star);
+  };
+
+  const animateStar = (star: number) => {
+    animatedValues[star - 1].value = withTiming(1.3, { duration: 150 }, () => {
+      animatedValues[star - 1].value = withTiming(1, { duration: 150 });
+    });
+    colorValues[star - 1].value = withTiming(1, { duration: 150 }, () => {
+      colorValues[star - 1].value = withTiming(0, { duration: 150 });
+    });
+  };
+
+  return (
+    <View style={{ flexDirection: 'row', justifyContent: 'center', gap: 5 }}>
+      {[1, 2, 3, 4, 5].map((star, index) => {
+        const animatedStyle = useAnimatedStyle(() => ({
+          transform: [{ scale: animatedValues[index].value }]
+        }));
+
+        const animatedColor = useAnimatedStyle(() => {
+          const color = interpolateColor(
+            colorValues[index].value,
+            [0, 1],
+            [Colors.DARK_BLUE, Colors.LIGHT_GRAY]
+          );
+          return { color };
+        });
+
+        return (
+          <TouchableOpacity key={star} onPress={() => handlePress(star)}>
+            <Animated.View style={animatedStyle}>
+              <Animated.Text style={animatedColor}>
+                <FontAwesome name={rating >= star ? 'star' : 'star-o'} size={22} />
+              </Animated.Text>
+            </Animated.View>
+          </TouchableOpacity>
+        );
+      })}
+    </View>
+  );
+};
+
+export default StarRating;

+ 2 - 0
src/screens/InAppScreens/TravelsScreen/Components/index.ts

@@ -1,3 +1,5 @@
 export * from './CustomButton';
 export * from './PhotoItem';
 export * from './PhotoEditModal';
+export * from './RateModal';
+export * from './StarRating';

+ 23 - 5
src/screens/InAppScreens/TravelsScreen/CountriesScreen/index.tsx

@@ -26,7 +26,7 @@ const CountriesScreen = () => {
   const { data, refetch } = useGetSlowQuery(String(token));
   const [megaSelectorVisible, setMegaSelectorVisible] = useState(false);
   const [selectedMega, setSelectedMega] = useState<{ name: string; id: number }>({
-    name: 'ALL',
+    name: 'ALL MEGAREGIONS',
     id: -1
   });
   const [total, setTotal] = useState({ slow: 0, visited: 0 });
@@ -46,15 +46,33 @@ const CountriesScreen = () => {
   useEffect(() => {
     if (slow && slow.length) {
       token && calcTotalScore();
-      setFilteredSlow(slow);
+      let newSlowData = slow;
+
+      if (search) {
+        newSlowData =
+          slow?.filter((item: any) => {
+            const itemData = item.country ? item.country.toLowerCase() : ''.toLowerCase();
+            const textData = search.toLowerCase();
+            return itemData.indexOf(textData) > -1;
+          }) ?? [];
+      }
+      setFilteredSlow(newSlowData);
     }
   }, [slow]);
 
   useEffect(() => {
+    const refetchData = async () => {
+      await refetch().then((res) => {
+        if (res.data) {
+          setSlow(res.data.slow);
+        }
+      });
+    };
+
     if (data && data.result === 'OK') {
       setSearch('');
       if (selectedMega.id === -1) {
-        refetch()
+        refetchData();
       } else {
         setSlow(data?.slow?.filter((item) => item.mega.includes(selectedMega.id)));
       }
@@ -187,10 +205,10 @@ const CountriesScreen = () => {
               style={styles.btnOption}
               onPress={() => {
                 setMegaSelectorVisible(false);
-                setSelectedMega({ name: 'ALL', id: -1 });
+                setSelectedMega({ name: 'ALL MEGAREGIONS', id: -1 });
               }}
             >
-              <Text style={styles.btnOptionText}>ALL</Text>
+              <Text style={styles.btnOptionText}>ALL MEGAREGIONS</Text>
             </TouchableOpacity>
             {data?.megaregions?.map((mega) => (
               <TouchableOpacity

+ 50 - 15
src/screens/InAppScreens/TravelsScreen/DareScreen/index.tsx

@@ -3,7 +3,7 @@ import { View, Text, TouchableOpacity, FlatList, Platform } from 'react-native';
 import * as Progress from 'react-native-progress';
 import { useFocusEffect, useNavigation } from '@react-navigation/native';
 
-import { Header, PageWrapper } from 'src/components';
+import { Header, Input, PageWrapper } from 'src/components';
 import { CustomButton } from '../Components';
 import MegaregionsModal from '../Components/MegaregionsModal';
 
@@ -21,9 +21,10 @@ import {
 import ChevronIcon from 'assets/icons/travels-screens/down-arrow.svg';
 import { NAVIGATION_PAGES } from 'src/types';
 import { useRegion } from 'src/contexts/RegionContext';
+import SearchIcon from 'assets/icons/search.svg';
 
 const DareScreen = () => {
-  const token = (storage.get('token', StoreType.STRING) as string);
+  const token = storage.get('token', StoreType.STRING) as string;
   const { data: megaregions } = useGetMegaregionsDareQuery(String(token), true);
   const [megaSelectorVisible, setMegaSelectorVisible] = useState(false);
   const [selectedMega, setSelectedMega] = useState<{ name: string; id: number }>({
@@ -41,12 +42,13 @@ const DareScreen = () => {
     setDareRegions,
     setUserData
   } = useRegion();
+  const [search, setSearch] = useState<string>('');
 
   useEffect(() => {
     if (megaregions && megaregions.result === 'OK') {
       setSelectedMega(megaregions.data[1]);
     }
-  }, [megaregions])
+  }, [megaregions]);
 
   useEffect(() => {
     if (dareRegions && dareRegions.length) {
@@ -66,32 +68,54 @@ const DareScreen = () => {
     }
   }, [selectedMega]);
 
-  useEffect(() => {
+  const applyFilters = () => {
+    let newDareData = dareRegions ?? [];
+
     switch (contentIndex) {
       case 1:
-        setFilteredDareRegions(dareRegions?.filter((item: DareRegion) => +item.visited <= 0) || []);
+        newDareData = dareRegions?.filter((item: DareRegion) => +item.visited <= 0) || [];
         break;
       case 2:
-        setFilteredDareRegions(dareRegions?.filter((item: DareRegion) => +item.visited > 0) || []);
+        newDareData = dareRegions?.filter((item: DareRegion) => +item.visited > 0) || [];
         break;
       case 3:
-        setFilteredDareRegions(dareRegions?.filter((item: DareRegion) => item.new === 1) || []);
+        newDareData = dareRegions?.filter((item: DareRegion) => item.new === 1) || [];
         break;
-      default:
-        setFilteredDareRegions(dareRegions);
     }
-  }, [contentIndex, dareRegions]);
+
+    if (search) {
+      newDareData = newDareData?.filter((item: DareRegion) => {
+        const itemData = item.name ? item.name.toLowerCase() : '';
+        return itemData.includes(search.toLowerCase());
+      });
+    }
+
+    setFilteredDareRegions(newDareData);
+  };
+
+  useEffect(() => {
+    applyFilters();
+  }, [contentIndex, dareRegions, search]);
+
+  useEffect(() => {
+    setSearch('');
+  }, [contentIndex, selectedMega]);
 
   const calcTotalCountries = () => {
     const visited = dareRegions?.filter((item: DareRegion) => +item.visited > 0).length || 0;
     setTotal(visited);
   };
 
+  const searchFilter = (text: string) => {
+    setSearch(text);
+  };
+
   const renderItem = ({ item }: { item: DareRegion }) => (
     <TouchableOpacity
       onPress={() => {
         setUserData({
           type: 'dare',
+          id: item.id,
           region_flag: item.flag1,
           region_name: item.name,
           visited: +item.visited > 0
@@ -131,6 +155,17 @@ const DareScreen = () => {
         <ChevronIcon width={18} height={18} />
       </TouchableOpacity>
 
+      <View style={{ marginBottom: 16 }}>
+        <Input
+          inputMode={'search'}
+          placeholder={'Search'}
+          onChange={(text) => searchFilter(text)}
+          value={search}
+          icon={<SearchIcon fill={'#C8C8C8'} width={14} height={14} />}
+          height={34}
+        />
+      </View>
+
       {token && (
         <View style={styles.buttonContainer}>
           <CustomButton
@@ -138,16 +173,16 @@ const DareScreen = () => {
             onPress={() => setContentIndex(0)}
             isActive={contentIndex === 0}
           />
-          <CustomButton
-            title="Not visited"
-            onPress={() => setContentIndex(1)}
-            isActive={contentIndex === 1}
-          />
           <CustomButton
             title="Visited"
             onPress={() => setContentIndex(2)}
             isActive={contentIndex === 2}
           />
+          <CustomButton
+            title="Not visited"
+            onPress={() => setContentIndex(1)}
+            isActive={contentIndex === 1}
+          />
           <CustomButton
             title="New"
             onPress={() => setContentIndex(3)}

+ 55 - 0
src/screens/InAppScreens/TravelsScreen/FixersCommentsScreen/index.tsx

@@ -0,0 +1,55 @@
+import React from 'react';
+import { View, Text } from 'react-native';
+import { FlashList } from '@shopify/flash-list';
+
+import { PageWrapper, Header } from 'src/components';
+import { styles } from './styles';
+import { Star } from '../Components/Star';
+
+const FixersCommentsScreen = ({ route }: { route: any }) => {
+  const comments = route.params?.comments;
+
+  const renderRatingStars = (rate: string) => {
+    const stars = [];
+
+    for (let i = 0; i < 5; i++) {
+      const filled = Math.min(Math.max(+rate - i, 0), 1);
+      stars.push(<Star key={i} filled={filled} />);
+    }
+    return <View style={styles.ratingContainer}>{stars}</View>;
+  };
+
+  const renderItem = ({ item }: { item: any }) => {
+    return (
+      <View style={styles.itemContainer}>
+        <View style={{ gap: 8 }}>
+          <Text style={styles.name}>{item.name}</Text>
+          {renderRatingStars(item.rate)}
+        </View>
+
+        <Text style={styles.comment}>{item.comment}</Text>
+      </View>
+    );
+  };
+
+  return (
+    <PageWrapper>
+      <Header label={route.params?.name} />
+
+      <FlashList
+        viewabilityConfig={{
+          waitForInteraction: true,
+          itemVisiblePercentThreshold: 50,
+          minimumViewTime: 1000
+        }}
+        estimatedItemSize={50}
+        data={comments}
+        renderItem={renderItem}
+        keyExtractor={(item, index) => index.toString()}
+        showsVerticalScrollIndicator={false}
+      />
+    </PageWrapper>
+  );
+};
+
+export default FixersCommentsScreen;

+ 29 - 0
src/screens/InAppScreens/TravelsScreen/FixersCommentsScreen/styles.tsx

@@ -0,0 +1,29 @@
+import { StyleSheet } from 'react-native';
+import { Colors } from 'src/theme';
+
+export const styles = StyleSheet.create({
+  itemContainer: {
+    paddingHorizontal: 16,
+    paddingVertical: 12,
+    backgroundColor: Colors.FILL_LIGHT,
+    borderRadius: 8,
+    gap: 12,
+    marginBottom: 16
+  },
+  ratingContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    gap: 2,
+    flex: 1
+  },
+  name: {
+    fontFamily: 'redhat-700',
+    color: Colors.DARK_BLUE,
+    fontSize: 14
+  },
+  comment: {
+    fontSize: 14,
+    color: Colors.DARK_BLUE,
+    fontWeight: '500'
+  }
+});

+ 174 - 0
src/screens/InAppScreens/TravelsScreen/FixersScreen/index.tsx

@@ -0,0 +1,174 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import { View, Text, TouchableOpacity, FlatList } from 'react-native';
+import { useFocusEffect, useNavigation } from '@react-navigation/native';
+
+import {
+  PageWrapper,
+  Header,
+  Modal,
+  FlatList as List,
+  WarningModal,
+  Loading
+} from 'src/components';
+import { StoreType, storage } from 'src/storage';
+import { NAVIGATION_PAGES } from 'src/types';
+import { styles } from './styles';
+import FixerItem from '../Components/FixerItem';
+import { RateModal } from '../Components';
+import { useGetCountriesQuery, useGetFixersQuery, usePostSaveRatingMutation } from '@api/fixers';
+
+import ChevronIcon from '../../../../../assets/icons/travels-screens/chevron-bottom.svg';
+import AddIcon from '../../../../../assets/icons/travels-screens/circle-plus.svg';
+import InfoIcon from '../../../../../assets/icons/info-solid.svg';
+
+const FixersScreen = ({ route }: { route: any }) => {
+  const token = storage.get('token', StoreType.STRING) as string;
+  const navigation = useNavigation();
+  const { data: countries } = useGetCountriesQuery(token, true);
+  const [selectedCountry, setSelectedCountry] = useState<{
+    id: number;
+    country: string;
+    flag: string;
+  } | null>(null);
+  const { data, refetch } = useGetFixersQuery(
+    token,
+    selectedCountry?.id as number,
+    selectedCountry ? true : false
+  );
+  const [isCountryPickerVisible, setCountryPickerVisible] = useState(false);
+  const [fixers, setFixers] = useState<any[]>([]);
+  const [isWarningModalVisible, setIsWarningModalVisible] = useState(false);
+  const [selectedFixer, setSelectedFixer] = useState<any | null>(null);
+  const { mutateAsync: saveRating } = usePostSaveRatingMutation();
+
+  useFocusEffect(
+    useCallback(() => {
+      const fetchData = async () => {
+        try {
+          await refetch();
+          navigation.setParams({ saved: false } as never);
+        } catch (error) {
+          console.error(error);
+        }
+      };
+
+      if (route.params?.saved) {
+        fetchData();
+      }
+    }, [route.params])
+  );
+
+  useEffect(() => {
+    if (countries && countries.data) {
+      setSelectedCountry(countries?.data[0]);
+    }
+  }, [countries]);
+
+  useEffect(() => {
+    if (data) {
+      const sortedFixers = data.data.sort((a, b) => {
+        if (b.year !== a.year) {
+          return b.year - a.year;
+        }
+        return b.month - a.month;
+      });
+      setFixers(sortedFixers);
+    }
+  }, [data]);
+
+  const onAddNewFixerPress = useCallback(() => {
+    navigation.navigate(NAVIGATION_PAGES.ADD_FIXER as never);
+  }, [navigation]);
+
+  if (!selectedCountry) return <Loading />;
+
+  const renderItem = ({ item }: { item: any }) => (
+    <FixerItem
+      item={item}
+      setSelectedFixer={setSelectedFixer}
+      country={selectedCountry}
+      setIsWarningModalVisible={setIsWarningModalVisible}
+    />
+  );
+
+  const handleSaveRating = async (values: any) => {
+    const { rating1, rating2, rating3, comment } = values;
+    await saveRating({
+      token,
+      fixer_id: selectedFixer.id,
+      rating1,
+      rating2,
+      rating3,
+      comment
+    });
+    setSelectedFixer(null);
+    refetch();
+  };
+
+  return (
+    <PageWrapper>
+      <Header
+        label="Fixers"
+        rightElement={
+          <TouchableOpacity
+            onPress={() => navigation.navigate(NAVIGATION_PAGES.FIXERS_INFO as never)}
+            style={{ width: 30 }}
+          >
+            <InfoIcon />
+          </TouchableOpacity>
+        }
+      />
+      <View style={styles.tabContainer}>
+        <TouchableOpacity style={styles.countrySelector} onPress={() => setCountryPickerVisible(true)}>
+          <Text style={[styles.countryText]}>{selectedCountry.country}</Text>
+          <ChevronIcon />
+        </TouchableOpacity>
+        <TouchableOpacity style={styles.addNewTab} onPress={onAddNewFixerPress}>
+          <AddIcon />
+          <Text style={styles.addNewTabText}>Add New Fixer</Text>
+        </TouchableOpacity>
+      </View>
+
+      <FlatList
+        data={fixers}
+        renderItem={renderItem}
+        keyExtractor={(item, index) => item.id.toString() + index.toString()}
+        style={styles.fixersList}
+        contentContainerStyle={styles.fixersListContentContainer}
+        showsVerticalScrollIndicator={false}
+      />
+
+      <Modal
+        onRequestClose={() => setCountryPickerVisible(false)}
+        headerTitle={'Select Country'}
+        visible={isCountryPickerVisible}
+      >
+        <List
+          itemObject={(object) => {
+            setSelectedCountry(object);
+            setCountryPickerVisible(false);
+          }}
+          initialData={countries?.data}
+          countries={true}
+        />
+      </Modal>
+
+      <WarningModal
+        type={'success'}
+        isVisible={isWarningModalVisible}
+        onClose={() => {
+          setIsWarningModalVisible(false);
+        }}
+        message="You can rate a fixer only once every 12 months."
+      />
+
+      <RateModal
+        selectedFixer={selectedFixer}
+        setSelectedFixer={setSelectedFixer}
+        saveRating={handleSaveRating}
+      />
+    </PageWrapper>
+  );
+};
+
+export default FixersScreen;

+ 39 - 0
src/screens/InAppScreens/TravelsScreen/FixersScreen/styles.tsx

@@ -0,0 +1,39 @@
+import { StyleSheet } from 'react-native';
+import { Colors } from 'src/theme';
+
+export const styles = StyleSheet.create({
+  tabContainer: { flexDirection: 'row', gap: 16, alignItems: 'center', marginBottom: 8 },
+  countrySelector: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    backgroundColor: Colors.FILL_LIGHT,
+    borderRadius: 4,
+    height: 34,
+    flex: 1,
+    paddingHorizontal: 16
+  },
+  countryText: {
+    fontSize: 14,
+    color: Colors.LIGHT_GRAY,
+    fontWeight: '500'
+  },
+  addNewTab: {
+    flex: 1,
+    backgroundColor: Colors.ORANGE,
+    height: 36,
+    borderRadius: 4,
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'center',
+    paddingHorizontal: 16,
+    gap: 4
+  },
+  addNewTabText: { fontSize: 14, color: Colors.WHITE, fontWeight: 'bold' },
+  fixersList: {
+    flex: 1
+  },
+  fixersListContentContainer: {
+    paddingBottom: 16
+  }
+});

+ 80 - 38
src/screens/InAppScreens/TravelsScreen/RegionsScreen/index.tsx

@@ -4,7 +4,7 @@ import * as Progress from 'react-native-progress';
 import { useFocusEffect, useNavigation } from '@react-navigation/native';
 import moment from 'moment';
 
-import { EditNmModal, Header, PageWrapper } from 'src/components';
+import { EditNmModal, Header, Input, PageWrapper } from 'src/components';
 import { CustomButton } from '../Components';
 import { NmRegionItem } from '../Components/MyRegionsItems/NmRegionItem';
 import { RegionItem } from '../Components/MyRegionsItems/RegionItem';
@@ -24,6 +24,7 @@ import { qualityOptions } from '../utils/constants';
 import ChevronIcon from 'assets/icons/travels-screens/down-arrow.svg';
 import { NAVIGATION_PAGES } from 'src/types';
 import { useRegion } from 'src/contexts/RegionContext';
+import SearchIcon from 'assets/icons/search.svg';
 
 const RegionsScreen = () => {
   const token = storage.get('token', StoreType.STRING) as string;
@@ -51,6 +52,7 @@ const RegionsScreen = () => {
   });
   const navigation = useNavigation();
   const { handleUpdateNMList: handleUpdateNM, nmRegions, setNmRegions, setUserData } = useRegion();
+  const [search, setSearch] = useState<string>('');
 
   useEffect(() => {
     const currentYear = moment().year();
@@ -121,57 +123,86 @@ const RegionsScreen = () => {
     }
   }, [selectedMega]);
 
-  useEffect(() => {
+  const applyFilters = () => {
+    let newNmData = nmRegions ?? [];
+    let newTccData = tccRegions ?? [];
+
     switch (contentIndex) {
-      case 0:
-        setFilteredNmRegions(nmRegions);
-        setFilteredTccRegions(tccRegions);
-        break;
       case 1:
-        setFilteredNmRegions(nmRegions?.filter((item: NmRegion) => item.visits <= 0) || []);
-        setFilteredTccRegions(tccRegions?.filter((item) => item.visited <= 0) || []);
+        newNmData = nmRegions?.filter((item: NmRegion) => item.visits <= 0) || [];
+        newTccData = tccRegions?.filter((item) => item.visited <= 0) || [];
         break;
       case 2:
-        setFilteredNmRegions(nmRegions?.filter((item: NmRegion) => item.visits > 0) || []);
-        setFilteredTccRegions(tccRegions?.filter((item) => item.visited > 0) || []);
+        newNmData = nmRegions?.filter((item: NmRegion) => item.visits > 0) || [];
+        newTccData = tccRegions?.filter((item) => item.visited > 0) || [];
         break;
     }
-  }, [contentIndex, nmRegions, tccRegions]);
+
+    if (search) {
+      newNmData = newNmData?.filter((item: NmRegion) => {
+        const itemData = item.region_name ? item.region_name.toLowerCase() : '';
+        return itemData.includes(search.toLowerCase());
+      });
+      newTccData = newTccData?.filter((item: TCCRegion) => {
+        const itemData = item.name ? item.name.toLowerCase() : '';
+        return itemData.includes(search.toLowerCase());
+      });
+    }
+
+    setFilteredNmRegions(newNmData);
+    setFilteredTccRegions(newTccData);
+  };
+
+  useEffect(() => {
+    applyFilters();
+  }, [contentIndex, nmRegions, tccRegions, search]);
+
+  useEffect(() => {
+    setSearch('');
+  }, [contentIndex, selectedMega]);
 
   useEffect(() => {
     if (megaregions && megaregions.result === 'OK') {
       setSelectedMega(megaregions.data[1]);
     }
-  }, [megaregions])
+  }, [megaregions]);
 
   const calcTotalCountries = () => {
     const visited = nmRegions?.filter((item: NmRegion) => item.visits > 0).length || 0;
     setTotal(visited);
   };
 
+  const searchFilter = (text: string) => {
+    setSearch(text);
+  };
+
   const renderItem = ({ item }: { item: NmRegion }) => (
-    <TouchableOpacity onPress={() => {
-      setUserData({
-        type: 'nm',
-        region_flag: item.flag_1,
-        region_name: item.region_name,
-        best_visit_quality: item.quality,
-        first_visit_year: item.year,
-        last_visit_year: item.last,
-        no_of_visits: item.visits,
-        visited: item.visits > 0,
-      });
-      navigation.navigate(
-      ...([
-        NAVIGATION_PAGES.REGION_PREVIEW,
-        {
-          regionId: item.id,
-          isTravelsScreen: true,
+    <TouchableOpacity
+      onPress={() => {
+        setUserData({
           type: 'nm',
-          disabled: token ? false : true,
-        }
-      ] as never)
-    )}}>
+          id: item.id,
+          region_flag: item.flag_1,
+          region_name: item.region_name,
+          best_visit_quality: item.quality,
+          first_visit_year: item.year,
+          last_visit_year: item.last,
+          no_of_visits: item.visits,
+          visited: item.visits > 0
+        });
+        navigation.navigate(
+          ...([
+            NAVIGATION_PAGES.REGION_PREVIEW,
+            {
+              regionId: item.id,
+              isTravelsScreen: true,
+              type: 'nm',
+              disabled: token ? false : true
+            }
+          ] as never)
+        );
+      }}
+    >
       <NmRegionItem
         item={item}
         openEditModal={handleOpenEditModal}
@@ -199,6 +230,17 @@ const RegionsScreen = () => {
         <ChevronIcon width={18} height={18} />
       </TouchableOpacity>
 
+      <View style={{ marginBottom: 16 }}>
+        <Input
+          inputMode={'search'}
+          placeholder={'Search'}
+          onChange={(text) => searchFilter(text)}
+          value={search}
+          icon={<SearchIcon fill={'#C8C8C8'} width={14} height={14} />}
+          height={34}
+        />
+      </View>
+
       {token && (
         <View style={styles.buttonContainer}>
           <CustomButton
@@ -206,16 +248,16 @@ const RegionsScreen = () => {
             onPress={() => setContentIndex(0)}
             isActive={contentIndex === 0}
           />
-          <CustomButton
-            title="Not visited"
-            onPress={() => setContentIndex(1)}
-            isActive={contentIndex === 1}
-          />
           <CustomButton
             title="Visited"
             onPress={() => setContentIndex(2)}
             isActive={contentIndex === 2}
           />
+          <CustomButton
+            title="Not visited"
+            onPress={() => setContentIndex(1)}
+            isActive={contentIndex === 1}
+          />
         </View>
       )}
 

+ 1 - 1
src/screens/InAppScreens/TravelsScreen/RegionsScreen/styles.tsx

@@ -10,7 +10,7 @@ export const styles = StyleSheet.create({
     borderRadius: 6,
     backgroundColor: Colors.FILL_LIGHT,
     justifyContent: 'space-between',
-    marginBottom: 16
+    marginBottom: 8
   },
   megaButtonText: {
     color: Colors.DARK_BLUE,

+ 4 - 4
src/screens/InAppScreens/TravelsScreen/SeriesItemScreen/index.tsx

@@ -82,16 +82,16 @@ export const SeriesItemScreen = ({ route }: { route: any }) => {
 
   const [routes] = useState([
     { key: 'all', title: 'All items' },
-    { key: 'new', title: 'New' },
+    { key: 'checked', title: 'Ticked' },
     { key: 'unchecked', title: 'Unticked' },
-    { key: 'checked', title: 'Ticked' }
+    { key: 'new', title: 'New' },
   ]);
 
   const handleIndexChange = (index: number) => {
     let dataToFilter = [...seriesData];
 
     switch (index) {
-      case 1:
+      case 3:
         dataToFilter = dataToFilter
           .map((group) => ({
             ...group,
@@ -108,7 +108,7 @@ export const SeriesItemScreen = ({ route }: { route: any }) => {
           }))
           .filter((group) => group.items.length > 0);
         break;
-      case 3:
+      case 1:
         dataToFilter = dataToFilter
           .map((group) => ({
             ...group,

+ 9 - 2
src/screens/InAppScreens/TravelsScreen/index.tsx

@@ -14,6 +14,7 @@ import SeriesIcon from '../../../../assets/icons/travels-section/series.svg';
 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';
+import FixersIcon from '../../../../assets/icons/travels-section/fixers.svg';
 import InfoIcon from 'assets/icons/info-solid.svg';
 
 const TravelsScreen = () => {
@@ -28,11 +29,17 @@ const TravelsScreen = () => {
     { label: 'Series', icon: SeriesIcon, page: NAVIGATION_PAGES.SERIES },
     { label: 'Earth', icon: EarthIcon, page: NAVIGATION_PAGES.EARTH },
     { label: 'Trips', icon: TripIcon, page: NAVIGATION_PAGES.TRIPS },
-    { label: 'Photos', icon: ImagesIcon, page: NAVIGATION_PAGES.PHOTOS }
+    { label: 'Photos', icon: ImagesIcon, page: NAVIGATION_PAGES.PHOTOS },
+    { label: 'Fixers', icon: FixersIcon, page: NAVIGATION_PAGES.FIXERS }
   ];
 
   const handlePress = (page: string) => {
-    if (!token && (page === NAVIGATION_PAGES.TRIPS || page === NAVIGATION_PAGES.PHOTOS)) {
+    if (
+      !token &&
+      (page === NAVIGATION_PAGES.TRIPS ||
+        page === NAVIGATION_PAGES.PHOTOS ||
+        page === NAVIGATION_PAGES.FIXERS)
+    ) {
       setIsModalVisible(true);
     } else {
       navigation.navigate(page as never);

+ 15 - 0
src/screens/InAppScreens/TravelsScreen/utils/constants.ts

@@ -19,3 +19,18 @@ export const noOfVisits = [
   { label: '9', value: 9 },
   { label: '10+', value: 10 }
 ];
+
+export const months = [
+  { label: 'January', value: 1 },
+  { label: 'February', value: 2 },
+  { label: 'March', value: 3 },
+  { label: 'April', value: 4 },
+  { label: 'May', value: 5 },
+  { label: 'June', value: 6 },
+  { label: 'July', value: 7 },
+  { label: 'August', value: 8 },
+  { label: 'September', value: 9 },
+  { label: 'October', value: 10 },
+  { label: 'November', value: 11 },
+  { label: 'December', value: 12 }
+];

+ 35 - 0
src/screens/InAppScreens/TravelsScreen/utils/types.ts

@@ -128,3 +128,38 @@ export interface DareRegion {
   new: 0 | 1;
   flag?: string;
 }
+
+export interface FixerType {
+  month: number;
+  year: number;
+  selectedCountries: number[];
+  name: string;
+  email: string;
+  phone: string;
+  website: string;
+  comment: string;
+  anonymous: boolean;
+}
+
+export interface FixersData {
+  id: number;
+  month: number;
+  year: number;
+  contact: string;
+  name: string;
+  email: string;
+  phone: string;
+  web: string;
+  comment: string;
+  added_by_uid: number;
+  added_by_name: string;
+  can_rate: 0 | 1;
+  can_edit: 0 | 1;
+  ratings: Rating[];
+}
+
+type Rating = {
+  rate: string;
+  name: string;
+  comment: string;
+};

+ 37 - 0
src/screens/InfoScreens/FixersInfoScreen/index.tsx

@@ -0,0 +1,37 @@
+import { FC } from 'react';
+import { ImageBackground, Text, ScrollView } from 'react-native';
+import type { NavigationProp } from '@react-navigation/native';
+
+import { Header, PageWrapper } from '../../../components';
+import { styles } from './styles';
+
+type Props = {
+  navigation: NavigationProp<any>;
+};
+
+export const FixersInfoScreen: FC<Props> = ({ navigation }) => {
+  return (
+    <PageWrapper>
+      <Header label={'Fixers'} />
+      <ImageBackground
+        style={styles.background}
+        source={require('../../../../assets/images/nm-background.png')}
+      >
+        <ScrollView
+          style={styles.wrapper}
+          showsVerticalScrollIndicator={false}
+          contentContainerStyle={styles.contentContainerStyle}
+        >
+          <Text style={styles.text}>
+            This section is meant to provide contact details of fixers in challenging places as
+            suggested by our own travellers.{'\n'}
+            {'\n'}Advertising is strictly not allowed - the idea is for you to recommend local
+            fixers/experts that you experienced directly and you would recommend. Only authenticated
+            users can add an entry!{'\n'}
+            {'\n'}Thanks for your input.
+          </Text>
+        </ScrollView>
+      </ImageBackground>
+    </PageWrapper>
+  );
+};

+ 16 - 0
src/screens/InfoScreens/FixersInfoScreen/styles.tsx

@@ -0,0 +1,16 @@
+import { StyleSheet } from 'react-native';
+import { Colors } from 'src/theme';
+
+export const styles = StyleSheet.create({
+  background: { height: '100%', flex: 1 },
+  contentContainerStyle: { gap: 16, paddingBottom: 16 },
+  wrapper: {
+    display: 'flex',
+    height: '100%'
+  },
+  text: {
+    fontSize: 14,
+    fontWeight: '400',
+    color: Colors.DARK_BLUE
+  }
+});

+ 1 - 0
src/screens/InfoScreens/index.ts

@@ -8,3 +8,4 @@ export * from './EarthInfoScreen';
 export * from './FirstStepsInfoScreen';
 export * from './RegionsInfoScreen';
 export * from './TripsInfoScreen';
+export * from './FixersInfoScreen';

+ 16 - 2
src/screens/RegisterScreen/EditAccount/index.tsx

@@ -1,5 +1,5 @@
 import React, { useCallback, useEffect, useState } from 'react';
-import { View, ScrollView } from 'react-native';
+import { View, ScrollView, Text } from 'react-native';
 import { Formik } from 'formik';
 import * as yup from 'yup';
 import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
@@ -15,12 +15,14 @@ import store from '../../../storage/zustand';
 import { NAVIGATION_PAGES } from '../../../types';
 import { fetchAndSaveStatistics } from 'src/database/statisticsService';
 import { usePostGetProfileInfoDataQuery } from '@api/user';
+import { Colors } from 'src/theme';
+import { getFontSize } from 'src/utils';
 
 const SignUpSchema = yup.object({
   first_name: yup.string().required(),
   last_name: yup.string().required(),
   date_of_birth: yup.string().required(),
-  homebase: yup.number().required(),
+  homebase: yup.number().required().min(1, 'Region of origin is required'),
   homebase2: yup.number().optional()
 });
 
@@ -161,6 +163,18 @@ const EditAccount = () => {
                     headerTitle={'Region of origin'}
                     selectedObject={(data) => props.setFieldValue('homebase', data.id)}
                   />
+                  {props.touched.homebase && props.errors.homebase ? (
+                    <Text
+                      style={{
+                        color: Colors.RED,
+                        fontSize: getFontSize(12),
+                        fontFamily: 'redhat-600',
+                        marginTop: 5
+                      }}
+                    >
+                      {props.errors.homebase}
+                    </Text>
+                  ) : null}
                   <ModalFlatList
                     headerTitle={'Second region'}
                     selectedObject={(data) => props.setFieldValue('homebase2', data.id)}

+ 19 - 4
src/types/api.ts

@@ -19,7 +19,8 @@ export enum API_ROUTE {
   SEARCH = 'search',
   PROFILE = 'profile',
   FRIENDS = 'friends',
-  COUNTRIES = 'countries'
+  COUNTRIES = 'countries',
+  FIXERS = 'fixers'
 }
 
 export enum API_ENDPOINT {
@@ -98,7 +99,7 @@ export enum API_ENDPOINT {
   GET_SUGGESTION_DATA = 'get-suggestion-data',
   SUBMIT_SUGGESTION = 'submit-suggestion',
   GET_PROGILE_DATA = 'get-profile-data',
-  GET_PROFILE_UPDATES = 'get-profile-updates',
+  GET_PROFILE_UPDATES = 'get-profile-updates-2',
   GET_FRIENDS = 'load-friends-app',
   SEND_FRIEND_REQUEST = 'send-friend-request',
   LOAD_FRIENDS_SETTINGS = 'load-friends-settings-app',
@@ -112,7 +113,14 @@ export enum API_ENDPOINT {
   GET_MAP_YEARS = 'get-map-years',
   GET_SERIES_LIST = 'get-list',
   SET_NOTIFICATION_TOKEN = 'save-notification-token',
-  CHECK_TOKEN = 'check-token'
+  CHECK_TOKEN = 'check-token',
+  GET_FIXERS_COUNTRIES = 'get-countries',
+  GET_ALL_FIXERS_COUNTRIES = 'get-all-countries',
+  GET_FIXERS = 'get-for-country',
+  SAVE_RATING = 'save-rating-app',
+  ADD_FIXER = 'add-fixer',
+  EDIT_FIXER = 'edit-fixer',
+  GET_UPDATE = 'get-update'
 }
 
 export enum API {
@@ -204,7 +212,14 @@ export enum API {
   GET_MAP_YEARS = `${API_ROUTE.PROFILE}/${API_ENDPOINT.GET_MAP_YEARS}`,
   GET_SERIES_LIST = `${API_ROUTE.SERIES}/${API_ENDPOINT.GET_SERIES_LIST}`,
   SET_NOTIFICATION_TOKEN = `${API_ROUTE.USER}/${API_ENDPOINT.SET_NOTIFICATION_TOKEN}`,
-  CHECK_TOKEN = `${API_ROUTE.APP}/${API_ENDPOINT.CHECK_TOKEN}`
+  CHECK_TOKEN = `${API_ROUTE.APP}/${API_ENDPOINT.CHECK_TOKEN}`,
+  GET_FIXERS_COUNTRIES = `${API_ROUTE.FIXERS}/${API_ENDPOINT.GET_FIXERS_COUNTRIES}`,
+  GET_ALL_FIXERS_COUNTRIES = `${API_ROUTE.FIXERS}/${API_ENDPOINT.GET_ALL_FIXERS_COUNTRIES}`,
+  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}`,
+  GET_UPDATE = `${API_ROUTE.PROFILE}/${API_ENDPOINT.GET_UPDATE}`
 }
 
 export type BaseAxiosError = AxiosError;

+ 4 - 0
src/types/navigation.ts

@@ -15,6 +15,7 @@ export enum NAVIGATION_PAGES {
   EARTH_INFO = 'earthInfo',
   TRIPS_INFO = 'tripsInfo',
   REGIONS_INFO = 'regionsInfo',
+  FIXERS_INFO = 'fixersInfo',
   IN_APP = 'inAppStack',
   IN_APP_MAP_TAB = 'Map',
   MAP_TAB = 'inAppMapTab',
@@ -57,4 +58,7 @@ export enum NAVIGATION_PAGES {
   MY_FRIENDS = 'inAppMyFriends',
   COUNTRY_PREVIEW = 'inAppCountryPreview',
   MENU_DRAWER = 'Menu',
+  FIXERS = 'inAppFixers',
+  ADD_FIXER = 'inAppAddFixer',
+  FIXERS_COMMENTS = 'inAppFixersComments'
 }

+ 1 - 1
src/utils/mapHelpers.ts

@@ -46,7 +46,7 @@ export const calculateMapCountry = (bbox: turf.BBox, center: number[]): any => {
     maxLat === undefined ||
     maxLng === undefined
   ) {
-    throw new Error("Invalid bbox coordinates");
+    console.error("Invalid bbox coordinates");
   }
 
   let latitudeDelta = Math.abs(maxLat - minLat) + padding;

+ 21 - 7
src/utils/request.ts

@@ -8,7 +8,11 @@ export const request = axios.create({
   timeout: 10000
 });
 
-export const setupInterceptors = ({ showError }: { showError: (message: string) => void }) => {
+export const setupInterceptors = ({
+  showError
+}: {
+  showError: (message: string, loginNeeded: boolean) => void;
+}) => {
   request.interceptors.request.use(
     (config) => {
       config.headers['App-Version'] = APP_VERSION;
@@ -23,21 +27,31 @@ export const setupInterceptors = ({ showError }: { showError: (message: string)
   request.interceptors.response.use(
     (response) => {
       if (response.data.result === 'ERROR' && response.data.result_description) {
-        setTimeout(() => {
-          showError(response.data.result_description);
-        }, 1000);
+        const showErrorWithDelay = (message: string, requiresLogin: boolean) => {
+          setTimeout(() => {
+            showError(message, requiresLogin);
+          }, 1000);
+        };
+
+        if (response.data?.login_needed && response.data.login_needed === 1) {
+          showErrorWithDelay(response.data.result_description, true);
+          return response;
+        }
+        showErrorWithDelay(response.data.result_description, false);
       }
       return response;
     },
     (error) => {
-      if (error.code === 'ECONNABORTED' || error.message === 'Network Error') {
+      if (error.code === 'ECONNABORTED') {
         error.isTimeout = true;
         showBanner('Slow internet connection!');
-
+        
+        return Promise.reject(error);
+      } else if (error.message === 'Network Error') {
         return Promise.reject(error);
       }
 
-      showError(error.message);
+      showError(error.message, false);
 
       return Promise.reject(error);
     }

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov