Viktoriia 10 months ago
parent
commit
2f38f97083
45 changed files with 1788 additions and 116 deletions
  1. 15 26
      Route.tsx
  2. 1 0
      assets/icons/travels-section/fixers.svg
  3. 0 9
      src/components/ErrorModal/index.tsx
  4. 3 1
      src/components/FlatList/index.tsx
  5. 11 2
      src/components/FlatList/item.tsx
  6. 1 2
      src/database/tilesService/index.ts
  7. 80 0
      src/modules/api/fixers/fixers-api.ts
  8. 8 0
      src/modules/api/fixers/fixers-query-keys.tsx
  9. 3 0
      src/modules/api/fixers/index.ts
  10. 6 0
      src/modules/api/fixers/queries/index.ts
  11. 17 0
      src/modules/api/fixers/queries/use-post-add-fixer.tsx
  12. 17 0
      src/modules/api/fixers/queries/use-post-edit-fixer.tsx
  13. 17 0
      src/modules/api/fixers/queries/use-post-get-all-countries.tsx
  14. 17 0
      src/modules/api/fixers/queries/use-post-get-countries.tsx
  15. 17 0
      src/modules/api/fixers/queries/use-post-get-for-country.tsx
  16. 17 0
      src/modules/api/fixers/queries/use-post-save-rating-app.tsx
  17. 2 2
      src/modules/api/user/queries/use-post-get-map-years.tsx
  18. 2 2
      src/modules/api/user/queries/use-post-get-profile-regions.tsx
  19. 4 4
      src/modules/api/user/user-api.tsx
  20. 3 2
      src/screens/InAppScreens/MapScreen/FilterModal/index.tsx
  21. 64 57
      src/screens/InAppScreens/MapScreen/index.tsx
  22. 1 1
      src/screens/InAppScreens/ProfileScreen/Components/PersonalInfo.tsx
  23. 339 0
      src/screens/InAppScreens/TravelsScreen/AddNewFixerScreen/index.tsx
  24. 91 0
      src/screens/InAppScreens/TravelsScreen/AddNewFixerScreen/styles.tsx
  25. 216 0
      src/screens/InAppScreens/TravelsScreen/Components/FixerItem/index.tsx
  26. 86 0
      src/screens/InAppScreens/TravelsScreen/Components/FixerItem/styles.tsx
  27. 151 0
      src/screens/InAppScreens/TravelsScreen/Components/RateModal/index.tsx
  28. 48 0
      src/screens/InAppScreens/TravelsScreen/Components/RateModal/styles.tsx
  29. 29 0
      src/screens/InAppScreens/TravelsScreen/Components/Star/index.tsx
  30. 71 0
      src/screens/InAppScreens/TravelsScreen/Components/StarRating/index.tsx
  31. 2 0
      src/screens/InAppScreens/TravelsScreen/Components/index.ts
  32. 55 0
      src/screens/InAppScreens/TravelsScreen/FixersCommentsScreen/index.tsx
  33. 29 0
      src/screens/InAppScreens/TravelsScreen/FixersCommentsScreen/styles.tsx
  34. 174 0
      src/screens/InAppScreens/TravelsScreen/FixersScreen/index.tsx
  35. 39 0
      src/screens/InAppScreens/TravelsScreen/FixersScreen/styles.tsx
  36. 9 2
      src/screens/InAppScreens/TravelsScreen/index.tsx
  37. 15 0
      src/screens/InAppScreens/TravelsScreen/utils/constants.ts
  38. 35 0
      src/screens/InAppScreens/TravelsScreen/utils/types.ts
  39. 37 0
      src/screens/InfoScreens/FixersInfoScreen/index.tsx
  40. 16 0
      src/screens/InfoScreens/FixersInfoScreen/styles.tsx
  41. 1 0
      src/screens/InfoScreens/index.ts
  42. 16 2
      src/screens/RegisterScreen/EditAccount/index.tsx
  43. 16 3
      src/types/api.ts
  44. 4 0
      src/types/navigation.ts
  45. 3 1
      src/utils/request.ts

+ 15 - 26
Route.tsx

@@ -1,7 +1,7 @@
 import React, { useEffect, useState } from 'react';
 import React, { useEffect, useState } from 'react';
 import { useFonts } from 'expo-font';
 import { useFonts } from 'expo-font';
 import * as SplashScreen from 'expo-splash-screen';
 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 * as Notifications from 'expo-notifications';
 
 
 import { createStackNavigator, TransitionPresets } from '@react-navigation/stack';
 import { createStackNavigator, TransitionPresets } from '@react-navigation/stack';
@@ -47,6 +47,9 @@ import AddRegionsScreen from 'src/screens/InAppScreens/TravelsScreen/AddRegionsS
 import CountriesScreen from 'src/screens/InAppScreens/TravelsScreen/CountriesScreen';
 import CountriesScreen from 'src/screens/InAppScreens/TravelsScreen/CountriesScreen';
 import RegionsScreen from 'src/screens/InAppScreens/TravelsScreen/RegionsScreen';
 import RegionsScreen from 'src/screens/InAppScreens/TravelsScreen/RegionsScreen';
 import DareScreen from 'src/screens/InAppScreens/TravelsScreen/DareScreen';
 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 { API, NAVIGATION_PAGES } from './src/types';
 import { storage, StoreType } from './src/storage';
 import { storage, StoreType } from './src/storage';
@@ -68,7 +71,8 @@ import {
   RegionsInfoScreen,
   RegionsInfoScreen,
   EarthInfoScreen,
   EarthInfoScreen,
   DareInfoScreen,
   DareInfoScreen,
-  TripsInfoScreen
+  TripsInfoScreen,
+  FixersInfoScreen
 } from 'src/screens/InfoScreens';
 } from 'src/screens/InfoScreens';
 import RegionViewScreen from 'src/screens/InAppScreens/MapScreen/RegionViewScreen';
 import RegionViewScreen from 'src/screens/InAppScreens/MapScreen/RegionViewScreen';
 import { enableScreens } from 'react-native-screens';
 import { enableScreens } from 'react-native-screens';
@@ -103,24 +107,8 @@ const Route = () => {
   });
   });
   const [dbLoaded, setDbLoaded] = useState(false);
   const [dbLoaded, setDbLoaded] = useState(false);
   const uid = storage.get('uid', StoreType.STRING);
   const uid = storage.get('uid', StoreType.STRING);
-  const [appState, setAppState] = useState<string>(AppState.currentState);
   const { updateNotificationStatus } = useNotification();
   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 () => {
   const checkNmToken = async () => {
     if (token && uid) {
     if (token && uid) {
       try {
       try {
@@ -282,10 +270,7 @@ const Route = () => {
               component={EditPersonalInfo}
               component={EditPersonalInfo}
             />
             />
             <ScreenStack.Screen name={NAVIGATION_PAGES.SETTINGS} component={Settings} />
             <ScreenStack.Screen name={NAVIGATION_PAGES.SETTINGS} component={Settings} />
-            <ScreenStack.Screen
-              name={NAVIGATION_PAGES.MY_FRIENDS}
-              component={MyFriendsScreen}
-            />
+            <ScreenStack.Screen name={NAVIGATION_PAGES.MY_FRIENDS} component={MyFriendsScreen} />
           </ScreenStack.Navigator>
           </ScreenStack.Navigator>
         )}
         )}
       </BottomTab.Screen>
       </BottomTab.Screen>
@@ -305,6 +290,12 @@ const Route = () => {
             <ScreenStack.Screen name={NAVIGATION_PAGES.COUNTRIES} component={CountriesScreen} />
             <ScreenStack.Screen name={NAVIGATION_PAGES.COUNTRIES} component={CountriesScreen} />
             <ScreenStack.Screen name={NAVIGATION_PAGES.REGIONS} component={RegionsScreen} />
             <ScreenStack.Screen name={NAVIGATION_PAGES.REGIONS} component={RegionsScreen} />
             <ScreenStack.Screen name={NAVIGATION_PAGES.DARE} component={DareScreen} />
             <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
             <ScreenStack.Screen
               name={NAVIGATION_PAGES.REGION_PREVIEW}
               name={NAVIGATION_PAGES.REGION_PREVIEW}
               component={RegionViewScreen}
               component={RegionViewScreen}
@@ -384,10 +375,7 @@ const Route = () => {
               component={UsersListScreen}
               component={UsersListScreen}
               options={regionViewScreenOptions}
               options={regionViewScreenOptions}
             />
             />
-            <ScreenStack.Screen
-              name={NAVIGATION_PAGES.MY_FRIENDS}
-              component={MyFriendsScreen}
-            />
+            <ScreenStack.Screen name={NAVIGATION_PAGES.MY_FRIENDS} component={MyFriendsScreen} />
           </ScreenStack.Navigator>
           </ScreenStack.Navigator>
         )}
         )}
       </BottomTab.Screen>
       </BottomTab.Screen>
@@ -425,6 +413,7 @@ const Route = () => {
       <ScreenStack.Screen name={NAVIGATION_PAGES.DARE_INFO} component={DareInfoScreen} />
       <ScreenStack.Screen name={NAVIGATION_PAGES.DARE_INFO} component={DareInfoScreen} />
       <ScreenStack.Screen name={NAVIGATION_PAGES.REGIONS_INFO} component={RegionsInfoScreen} />
       <ScreenStack.Screen name={NAVIGATION_PAGES.REGIONS_INFO} component={RegionsInfoScreen} />
       <ScreenStack.Screen name={NAVIGATION_PAGES.TRIPS_INFO} component={TripsInfoScreen} />
       <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.EARTH_INFO} component={EarthInfoScreen} />
       <ScreenStack.Screen name={NAVIGATION_PAGES.IN_APP}>
       <ScreenStack.Screen name={NAVIGATION_PAGES.IN_APP}>
         {() => (
         {() => (

File diff suppressed because it is too large
+ 1 - 0
assets/icons/travels-section/fixers.svg


+ 0 - 9
src/components/ErrorModal/index.tsx

@@ -9,20 +9,11 @@ import { Colors } from 'src/theme';
 import CloseIcon from 'assets/icons/close.svg';
 import CloseIcon from 'assets/icons/close.svg';
 import { ButtonVariants } from 'src/types/components';
 import { ButtonVariants } from 'src/types/components';
 import { Button } from '../Button';
 import { Button } from '../Button';
-import { CommonActions, useNavigation } from '@react-navigation/native';
-import { NAVIGATION_PAGES } from 'src/types';
 
 
 export const ErrorModal = () => {
 export const ErrorModal = () => {
   const { error, hideError } = useError();
   const { error, hideError } = useError();
-  const navigation = useNavigation();
 
 
   const handleClose = () => {
   const handleClose = () => {
-    navigation.dispatch(
-      CommonActions.reset({
-        index: 1,
-        routes: [{ name: NAVIGATION_PAGES.IN_APP_MAP_TAB }]
-      })
-    );
     hideError();
     hideError();
   };
   };
 
 

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

@@ -12,11 +12,12 @@ type Props = {
   itemObject: (object: any) => void;
   itemObject: (object: any) => void;
   initialData?: ItemData[] | string[];
   initialData?: ItemData[] | string[];
   date?: boolean;
   date?: boolean;
+  countries?: boolean;
 };
 };
 
 
 //TODO: rework to generic types + custom props
 //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 [selectedObject, setSelectedObject] = useState<{ name: string; id: number } | string>();
   const [search, setSearch] = useState('');
   const [search, setSearch] = useState('');
   const [filteredData, setFilteredData] = useState<ItemData[] | string[]>([]);
   const [filteredData, setFilteredData] = useState<ItemData[] | string[]>([]);
@@ -73,6 +74,7 @@ export const FlatList: FC<Props> = ({ itemObject, initialData, date }) => {
         backgroundColor={backgroundColor}
         backgroundColor={backgroundColor}
         initial={initialData ? true : false}
         initial={initialData ? true : false}
         date={date}
         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 MarkSVG from '../../../assets/icons/mark.svg';
 import { API_HOST } from '../../constants';
 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('–') || '';
   const name = initial && date ? item : initial ? item.country : item.name?.split('–') || '';
 
 
   return (
   return (
@@ -28,7 +36,7 @@ export const Item = ({ item, onPress, backgroundColor, selected, initial, date }
             <Text style={[styles.title, { color: Colors.DARK_BLUE, flexShrink: 1 }]}>
             <Text style={[styles.title, { color: Colors.DARK_BLUE, flexShrink: 1 }]}>
               {initial ? (name as string) : (name as string[])[0]}
               {initial ? (name as string) : (name as string[])[0]}
             </Text>
             </Text>
-            {initial && !date && item.country !== 'All Regions' && (
+            {initial && !date && item.country !== 'All Regions' && !countries && (
               <View style={styles.regionIndicator}>
               <View style={styles.regionIndicator}>
                 <Text style={[styles.text, { color: Colors.WHITE, fontWeight: 'bold' }]}>
                 <Text style={[styles.text, { color: Colors.WHITE, fontWeight: 'bold' }]}>
                   {item.dare ? 'DARE' : 'NM'}
                   {item.dare ? 'DARE' : 'NM'}
@@ -63,4 +71,5 @@ type ItemProps = {
   selected: boolean;
   selected: boolean;
   initial?: boolean;
   initial?: boolean;
   date?: boolean;
   date?: boolean;
+  countries?: boolean;
 };
 };

+ 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> {
 export async function initTilesDownload(userId: string): Promise<void> {
   let tileTypes: TileType[] = [
   let tileTypes: TileType[] = [
     {url: '/tiles_osm', type: 'background', maxZoom: 5},
     {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) {
   for (const type of tileTypes) {

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

+ 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';
 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>({
   return useQuery<PostGetMapYearsReturn, BaseAxiosError>({
     queryKey: userQueryKeys.getMapYears(userId),
     queryKey: userQueryKeys.getMapYears(userId),
     queryFn: async () => {
     queryFn: async () => {
-      const response = await userApi.getMapYears(userId);
+      const response = await userApi.getMapYears(token, userId);
       return response.data;
       return response.data;
     },
     },
     enabled
     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';
 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>({
   return useQuery<PostGetProfileRegionsReturn, BaseAxiosError>({
     queryKey: userQueryKeys.getProfileRegions(uid, type),
     queryKey: userQueryKeys.getProfileRegions(uid, type),
     queryFn: async () => {
     queryFn: async () => {
-      const response = await userApi.getProfileRegions(uid, type);
+      const response = await userApi.getProfileRegions(token, uid, type);
       return response.data;
       return response.data;
     }
     }
   });
   });

+ 4 - 4
src/modules/api/user/user-api.tsx

@@ -313,8 +313,8 @@ export const userApi = {
     request.postForm<Exclude<PostGetProfileInfoReturn, { email: null }>>(API.PROFILE_INFO_PUBLIC, {
     request.postForm<Exclude<PostGetProfileInfoReturn, { email: null }>>(API.PROFILE_INFO_PUBLIC, {
       uid
       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) =>
   getProfileInfoData: (token: string, profile_id: number) =>
     request.postForm<PostGetProfileDataReturn>(API.GET_PROGILE_DATA, {
     request.postForm<PostGetProfileDataReturn>(API.GET_PROGILE_DATA, {
       token,
       token,
@@ -325,6 +325,6 @@ export const userApi = {
       token,
       token,
       profile_id
       profile_id
     }),
     }),
-  getMapYears: (profile_id: number) =>
-    request.postForm<PostGetMapYearsReturn>(API.GET_MAP_YEARS, { profile_id })
+  getMapYears: (token: string, profile_id: number) =>
+    request.postForm<PostGetMapYearsReturn>(API.GET_MAP_YEARS, { token, profile_id })
 };
 };

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

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

+ 64 - 57
src/screens/InAppScreens/MapScreen/index.tsx

@@ -465,19 +465,20 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation, route }) => {
               refreshDatabases();
               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', ...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;
           return;
         }
         }
@@ -524,34 +525,37 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation, route }) => {
       zoomLevel < 7 && mapRef.current?.animateToRegion(region, 1000);
       zoomLevel < 7 && mapRef.current?.animateToRegion(region, 1000);
 
 
       if (tableName === 'regions') {
       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', ...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 {
       } 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', ...data });
+              }
             }
             }
-          }
-        );
+          ));
         setProcessedMarkers([]);
         setProcessedMarkers([]);
       }
       }
     } else {
     } else {
@@ -582,19 +586,20 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation, route }) => {
           refreshDatabases();
           refreshDatabases();
         });
         });
 
 
-      await mutateCountriesData(
-        { id: +id, 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);
+      token &&
+        (await mutateCountriesData(
+          { id: +id, 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);
+              mapRef.current?.animateToRegion(region, 1000);
+            }
           }
           }
-        }
-      );
+        ));
 
 
       return;
       return;
     }
     }
@@ -642,18 +647,19 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation, route }) => {
               }
               }
             }
             }
           ));
           ));
-        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 {
       } else {
         token &&
         token &&
           (await mutateUserDataDare(
           (await mutateUserDataDare(
@@ -684,6 +690,7 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation, route }) => {
       offlineMode={!isConnected}
       offlineMode={!isConnected}
       opacity={opacity}
       opacity={opacity}
       zIndex={zIndex}
       zIndex={zIndex}
+      tileSize={256}
     />
     />
   );
   );
 
 
@@ -1026,7 +1033,7 @@ const MapScreen: React.FC<MapScreenProps> = ({ navigation, route }) => {
         setVisitedTiles={setVisitedTiles}
         setVisitedTiles={setVisitedTiles}
         setSeriesFilter={setSeriesFilter}
         setSeriesFilter={setSeriesFilter}
         isPublicView={false}
         isPublicView={false}
-        isLogged={!!token}
+        isLogged={token ? true : false}
       />
       />
       <EditModal
       <EditModal
         isVisible={isEditSlowModalVisible}
         isVisible={isEditSlowModalVisible}

+ 1 - 1
src/screens/InAppScreens/ProfileScreen/Components/PersonalInfo.tsx

@@ -89,7 +89,7 @@ export const PersonalInfo: FC<PersonalInfoProps> = ({
     title: ''
     title: ''
   });
   });
 
 
-  const { data: regions } = usePostGetProfileRegions(userId, type);
+  const { data: regions } = usePostGetProfileRegions(token as string, userId, type);
 
 
   useEffect(() => {
   useEffect(() => {
     if (data.isFriend === 1) {
     if (data.isFriend === 1) {

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

@@ -0,0 +1,339 @@
+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().url('invalid URL format').required('website is required'),
+  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"
+                height={64}
+                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
+  }
+});

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

@@ -0,0 +1,216 @@
+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>
+    );
+  };
+
+  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={() => Linking.openURL(item.web)}>
+              <Text style={[styles.linkText]}>{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 './CustomButton';
 export * from './PhotoItem';
 export * from './PhotoItem';
 export * from './PhotoEditModal';
 export * from './PhotoEditModal';
+export * from './RateModal';
+export * from './StarRating';

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

+ 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 EarthIcon from '../../../../assets/icons/travels-section/earth.svg';
 import TripIcon from '../../../../assets/icons/travels-section/trip.svg';
 import TripIcon from '../../../../assets/icons/travels-section/trip.svg';
 import ImagesIcon from '../../../../assets/icons/travels-section/images.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';
 import InfoIcon from 'assets/icons/info-solid.svg';
 
 
 const TravelsScreen = () => {
 const TravelsScreen = () => {
@@ -28,11 +29,17 @@ const TravelsScreen = () => {
     { label: 'Series', icon: SeriesIcon, page: NAVIGATION_PAGES.SERIES },
     { label: 'Series', icon: SeriesIcon, page: NAVIGATION_PAGES.SERIES },
     { label: 'Earth', icon: EarthIcon, page: NAVIGATION_PAGES.EARTH },
     { label: 'Earth', icon: EarthIcon, page: NAVIGATION_PAGES.EARTH },
     { label: 'Trips', icon: TripIcon, page: NAVIGATION_PAGES.TRIPS },
     { 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) => {
   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);
       setIsModalVisible(true);
     } else {
     } else {
       navigation.navigate(page as never);
       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: '9', value: 9 },
   { label: '10+', value: 10 }
   { 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;
   new: 0 | 1;
   flag?: string;
   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 './FirstStepsInfoScreen';
 export * from './RegionsInfoScreen';
 export * from './RegionsInfoScreen';
 export * from './TripsInfoScreen';
 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 React, { useCallback, useEffect, useState } from 'react';
-import { View, ScrollView } from 'react-native';
+import { View, ScrollView, Text } from 'react-native';
 import { Formik } from 'formik';
 import { Formik } from 'formik';
 import * as yup from 'yup';
 import * as yup from 'yup';
 import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
 import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
@@ -15,12 +15,14 @@ import store from '../../../storage/zustand';
 import { NAVIGATION_PAGES } from '../../../types';
 import { NAVIGATION_PAGES } from '../../../types';
 import { fetchAndSaveStatistics } from 'src/database/statisticsService';
 import { fetchAndSaveStatistics } from 'src/database/statisticsService';
 import { usePostGetProfileInfoDataQuery } from '@api/user';
 import { usePostGetProfileInfoDataQuery } from '@api/user';
+import { Colors } from 'src/theme';
+import { getFontSize } from 'src/utils';
 
 
 const SignUpSchema = yup.object({
 const SignUpSchema = yup.object({
   first_name: yup.string().required(),
   first_name: yup.string().required(),
   last_name: yup.string().required(),
   last_name: yup.string().required(),
   date_of_birth: 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()
   homebase2: yup.number().optional()
 });
 });
 
 
@@ -161,6 +163,18 @@ const EditAccount = () => {
                     headerTitle={'Region of origin'}
                     headerTitle={'Region of origin'}
                     selectedObject={(data) => props.setFieldValue('homebase', data.id)}
                     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
                   <ModalFlatList
                     headerTitle={'Second region'}
                     headerTitle={'Second region'}
                     selectedObject={(data) => props.setFieldValue('homebase2', data.id)}
                     selectedObject={(data) => props.setFieldValue('homebase2', data.id)}

+ 16 - 3
src/types/api.ts

@@ -19,7 +19,8 @@ export enum API_ROUTE {
   SEARCH = 'search',
   SEARCH = 'search',
   PROFILE = 'profile',
   PROFILE = 'profile',
   FRIENDS = 'friends',
   FRIENDS = 'friends',
-  COUNTRIES = 'countries'
+  COUNTRIES = 'countries',
+  FIXERS = 'fixers'
 }
 }
 
 
 export enum API_ENDPOINT {
 export enum API_ENDPOINT {
@@ -112,7 +113,13 @@ export enum API_ENDPOINT {
   GET_MAP_YEARS = 'get-map-years',
   GET_MAP_YEARS = 'get-map-years',
   GET_SERIES_LIST = 'get-list',
   GET_SERIES_LIST = 'get-list',
   SET_NOTIFICATION_TOKEN = 'save-notification-token',
   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'
 }
 }
 
 
 export enum API {
 export enum API {
@@ -204,7 +211,13 @@ export enum API {
   GET_MAP_YEARS = `${API_ROUTE.PROFILE}/${API_ENDPOINT.GET_MAP_YEARS}`,
   GET_MAP_YEARS = `${API_ROUTE.PROFILE}/${API_ENDPOINT.GET_MAP_YEARS}`,
   GET_SERIES_LIST = `${API_ROUTE.SERIES}/${API_ENDPOINT.GET_SERIES_LIST}`,
   GET_SERIES_LIST = `${API_ROUTE.SERIES}/${API_ENDPOINT.GET_SERIES_LIST}`,
   SET_NOTIFICATION_TOKEN = `${API_ROUTE.USER}/${API_ENDPOINT.SET_NOTIFICATION_TOKEN}`,
   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}`
 }
 }
 
 
 export type BaseAxiosError = AxiosError;
 export type BaseAxiosError = AxiosError;

+ 4 - 0
src/types/navigation.ts

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

+ 3 - 1
src/utils/request.ts

@@ -30,10 +30,12 @@ export const setupInterceptors = ({ showError }: { showError: (message: string)
       return response;
       return response;
     },
     },
     (error) => {
     (error) => {
-      if (error.code === 'ECONNABORTED' || error.message === 'Network Error') {
+      if (error.code === 'ECONNABORTED') {
         error.isTimeout = true;
         error.isTimeout = true;
         showBanner('Slow internet connection!');
         showBanner('Slow internet connection!');
 
 
+        return Promise.reject(error);
+      } else if (error.message === 'Network Error') {
         return Promise.reject(error);
         return Promise.reject(error);
       }
       }
 
 

Some files were not shown because too many files changed in this diff