Browse Source

trips screens

Viktoriia 1 year ago
parent
commit
ad7f164d49
39 changed files with 1686 additions and 33 deletions
  1. 4 0
      Route.tsx
  2. 10 0
      assets/icons/travels-screens/calendar.svg
  3. 10 0
      assets/icons/travels-screens/down-arrow.svg
  4. 1 1
      assets/icons/travels-screens/save.svg
  5. 1 1
      assets/icons/travels-screens/trash-solid.svg
  6. 7 2
      src/components/Button/index.tsx
  7. 19 2
      src/components/Calendars/RangeCalendar/index.tsx
  8. 7 4
      src/components/Calendars/RangeCalendar/style.ts
  9. 4 2
      src/components/Input/index.tsx
  10. 12 0
      src/components/WarningModal/index.tsx
  11. 2 1
      src/components/WarningModal/styles.tsx
  12. 3 0
      src/modules/api/trips/index.ts
  13. 7 0
      src/modules/api/trips/queries/index.ts
  14. 22 0
      src/modules/api/trips/queries/use-post-delete-trip.tsx
  15. 17 0
      src/modules/api/trips/queries/use-post-get-regions-for-trips.tsx
  16. 17 0
      src/modules/api/trips/queries/use-post-get-trip.tsx
  17. 17 0
      src/modules/api/trips/queries/use-post-get-trips-for-year.tsx
  18. 17 0
      src/modules/api/trips/queries/use-post-get-trips-years.tsx
  19. 16 0
      src/modules/api/trips/queries/use-post-set-new-trip.tsx
  20. 17 0
      src/modules/api/trips/queries/use-post-set-update-trip.tsx
  21. 94 0
      src/modules/api/trips/trips-api.tsx
  22. 9 0
      src/modules/api/trips/trips-query-keys.tsx
  23. 344 0
      src/screens/InAppScreens/TravelsScreen/AddNewTripScreen/index.tsx
  24. 100 0
      src/screens/InAppScreens/TravelsScreen/AddNewTripScreen/styles.tsx
  25. 1 1
      src/screens/InAppScreens/TravelsScreen/AddPhotoScreen/index.tsx
  26. 282 0
      src/screens/InAppScreens/TravelsScreen/AddRegionsScreen/index.tsx
  27. 73 0
      src/screens/InAppScreens/TravelsScreen/AddRegionsScreen/styles.tsx
  28. 95 0
      src/screens/InAppScreens/TravelsScreen/Components/RegionItem/index.tsx
  29. 101 0
      src/screens/InAppScreens/TravelsScreen/Components/RegionItem/styles.tsx
  30. 91 0
      src/screens/InAppScreens/TravelsScreen/Components/TripItem/index.tsx
  31. 100 0
      src/screens/InAppScreens/TravelsScreen/Components/TripItem/styles.tsx
  32. 109 13
      src/screens/InAppScreens/TravelsScreen/TripsScreen/index.tsx
  33. 9 2
      src/screens/InAppScreens/TravelsScreen/TripsScreen/styles.tsx
  34. 11 0
      src/screens/InAppScreens/TravelsScreen/utils/calculateRegion.ts
  35. 8 0
      src/screens/InAppScreens/TravelsScreen/utils/constants.ts
  36. 27 0
      src/screens/InAppScreens/TravelsScreen/utils/types.ts
  37. 2 1
      src/theme.ts
  38. 18 3
      src/types/api.ts
  39. 2 0
      src/types/navigation.ts

+ 4 - 0
Route.tsx

@@ -35,6 +35,8 @@ import PhotosScreen from 'src/screens/InAppScreens/TravelsScreen/PhotosScreen';
 import MorePhotosScreen from 'src/screens/InAppScreens/TravelsScreen/MorePhotosScreen';
 import AddPhotoScreen from 'src/screens/InAppScreens/TravelsScreen/AddPhotoScreen';
 import TripsScreen from 'src/screens/InAppScreens/TravelsScreen/TripsScreen';
+import AddNewTripScreen from 'src/screens/InAppScreens/TravelsScreen/AddNewTripScreen';
+import AddRegionsScreen from 'src/screens/InAppScreens/TravelsScreen/AddRegionsScreen';
 
 import { NAVIGATION_PAGES } from './src/types';
 import { storage, StoreType } from './src/storage';
@@ -173,6 +175,8 @@ const Route = () => {
                   <ScreenStack.Screen name={NAVIGATION_PAGES.MORE_PHOTOS} component={MorePhotosScreen} />
                   <ScreenStack.Screen name={NAVIGATION_PAGES.ADD_PHOTO} component={AddPhotoScreen} />
                   <ScreenStack.Screen name={NAVIGATION_PAGES.TRIPS} component={TripsScreen} />
+                  <ScreenStack.Screen name={NAVIGATION_PAGES.ADD_TRIP} component={AddNewTripScreen} />
+                  <ScreenStack.Screen name={NAVIGATION_PAGES.ADD_REGIONS} component={AddRegionsScreen} />
                 </ScreenStack.Navigator>
               )}
             </BottomTab.Screen>

+ 10 - 0
assets/icons/travels-screens/calendar.svg

@@ -0,0 +1,10 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_2039_19282)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8.5 2.00195C9.05228 2.00195 9.5 2.44967 9.5 3.00195V4.00195H14.5V3.00195C14.5 2.44967 14.9477 2.00195 15.5 2.00195C16.0523 2.00195 16.5 2.44967 16.5 3.00195V4.00195H18.5C20.1569 4.00195 21.5 5.3451 21.5 7.00195V18.002C21.5 19.6588 20.1569 21.002 18.5 21.002H5.5C3.84315 21.002 2.5 19.6588 2.5 18.002V7.00195C2.5 5.3451 3.84315 4.00195 5.5 4.00195H7.5V3.00195C7.5 2.44967 7.94772 2.00195 8.5 2.00195ZM7.5 6.00195H5.5C4.94772 6.00195 4.5 6.44967 4.5 7.00195V9.50195H19.5V7.00195C19.5 6.44967 19.0523 6.00195 18.5 6.00195H16.5V7.00195C16.5 7.55424 16.0523 8.00195 15.5 8.00195C14.9477 8.00195 14.5 7.55424 14.5 7.00195V6.00195H9.5V7.00195C9.5 7.55424 9.05228 8.00195 8.5 8.00195C7.94772 8.00195 7.5 7.55424 7.5 7.00195V6.00195ZM19.5 11.502H4.5V18.002C4.5 18.5542 4.94772 19.002 5.5 19.002H18.5C19.0523 19.002 19.5 18.5542 19.5 18.002V11.502Z"/>
+</g>
+<defs>
+<clipPath id="clip0_2039_19282">
+<rect width="24" height="24" fill="white" transform="translate(0 0.00195312)"/>
+</clipPath>
+</defs>
+</svg>

+ 10 - 0
assets/icons/travels-screens/down-arrow.svg

@@ -0,0 +1,10 @@
+<svg width="17" height="16" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_2039_18224)">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M14.1772 5.46104C14.4762 5.8348 14.4156 6.38019 14.0419 6.67919L9.04189 10.6792C8.72537 10.9324 8.27561 10.9324 7.95908 10.6792L2.95909 6.67919C2.58533 6.38019 2.52473 5.8348 2.82373 5.46104C3.12274 5.08728 3.66813 5.02668 4.04189 5.32569L8.50049 8.89257L12.9591 5.32569C13.3328 5.02668 13.8782 5.08728 14.1772 5.46104Z" fill="#0F3F4F"/>
+</g>
+<defs>
+<clipPath id="clip0_2039_18224">
+<rect width="16" height="16" fill="white" transform="translate(0.5 0.00195312)"/>
+</clipPath>
+</defs>
+</svg>

+ 1 - 1
assets/icons/travels-screens/save.svg

@@ -1,6 +1,6 @@
 <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
 <g clip-path="url(#clip0_1937_23992)">
-<path d="M2.28571 0C1.025 0 0 1.025 0 2.28571V13.7143C0 14.975 1.025 16 2.28571 16H13.7143C14.975 16 16 14.975 16 13.7143V5.04643C16 4.43929 15.7607 3.85714 15.3321 3.42857L12.5714 0.667857C12.1429 0.239286 11.5607 0 10.9536 0H2.28571ZM2.28571 3.42857C2.28571 2.79643 2.79643 2.28571 3.42857 2.28571H10.2857C10.9179 2.28571 11.4286 2.79643 11.4286 3.42857V5.71429C11.4286 6.34643 10.9179 6.85714 10.2857 6.85714H3.42857C2.79643 6.85714 2.28571 6.34643 2.28571 5.71429V3.42857ZM8 9.14286C8.60621 9.14286 9.18759 9.38367 9.61624 9.81233C10.0449 10.241 10.2857 10.8224 10.2857 11.4286C10.2857 12.0348 10.0449 12.6162 9.61624 13.0448C9.18759 13.4735 8.60621 13.7143 8 13.7143C7.39379 13.7143 6.81241 13.4735 6.38376 13.0448C5.9551 12.6162 5.71429 12.0348 5.71429 11.4286C5.71429 10.8224 5.9551 10.241 6.38376 9.81233C6.81241 9.38367 7.39379 9.14286 8 9.14286Z" fill="#0F3F4F"/>
+<path d="M2.28571 0C1.025 0 0 1.025 0 2.28571V13.7143C0 14.975 1.025 16 2.28571 16H13.7143C14.975 16 16 14.975 16 13.7143V5.04643C16 4.43929 15.7607 3.85714 15.3321 3.42857L12.5714 0.667857C12.1429 0.239286 11.5607 0 10.9536 0H2.28571ZM2.28571 3.42857C2.28571 2.79643 2.79643 2.28571 3.42857 2.28571H10.2857C10.9179 2.28571 11.4286 2.79643 11.4286 3.42857V5.71429C11.4286 6.34643 10.9179 6.85714 10.2857 6.85714H3.42857C2.79643 6.85714 2.28571 6.34643 2.28571 5.71429V3.42857ZM8 9.14286C8.60621 9.14286 9.18759 9.38367 9.61624 9.81233C10.0449 10.241 10.2857 10.8224 10.2857 11.4286C10.2857 12.0348 10.0449 12.6162 9.61624 13.0448C9.18759 13.4735 8.60621 13.7143 8 13.7143C7.39379 13.7143 6.81241 13.4735 6.38376 13.0448C5.9551 12.6162 5.71429 12.0348 5.71429 11.4286C5.71429 10.8224 5.9551 10.241 6.38376 9.81233C6.81241 9.38367 7.39379 9.14286 8 9.14286Z"/>
 </g>
 <defs>
 <clipPath id="clip0_1937_23992">

+ 1 - 1
assets/icons/travels-screens/trash-solid.svg

@@ -1,3 +1,3 @@
 <svg width="15" height="16" viewBox="0 0 15 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path d="M4.725 0.553125C4.89375 0.2125 5.24062 0 5.61875 0H9.38125C9.75938 0 10.1062 0.2125 10.275 0.553125L10.5 1H13.5C14.0531 1 14.5 1.44687 14.5 2C14.5 2.55312 14.0531 3 13.5 3H1.5C0.946875 3 0.5 2.55312 0.5 2C0.5 1.44687 0.946875 1 1.5 1H4.5L4.725 0.553125ZM1.5 4H13.5V14C13.5 15.1031 12.6031 16 11.5 16H3.5C2.39688 16 1.5 15.1031 1.5 14V4ZM4.5 6C4.225 6 4 6.225 4 6.5V13.5C4 13.775 4.225 14 4.5 14C4.775 14 5 13.775 5 13.5V6.5C5 6.225 4.775 6 4.5 6ZM7.5 6C7.225 6 7 6.225 7 6.5V13.5C7 13.775 7.225 14 7.5 14C7.775 14 8 13.775 8 13.5V6.5C8 6.225 7.775 6 7.5 6ZM10.5 6C10.225 6 10 6.225 10 6.5V13.5C10 13.775 10.225 14 10.5 14C10.775 14 11 13.775 11 13.5V6.5C11 6.225 10.775 6 10.5 6Z" fill="#0F3F4F"/>
+<path d="M4.725 0.553125C4.89375 0.2125 5.24062 0 5.61875 0H9.38125C9.75938 0 10.1062 0.2125 10.275 0.553125L10.5 1H13.5C14.0531 1 14.5 1.44687 14.5 2C14.5 2.55312 14.0531 3 13.5 3H1.5C0.946875 3 0.5 2.55312 0.5 2C0.5 1.44687 0.946875 1 1.5 1H4.5L4.725 0.553125ZM1.5 4H13.5V14C13.5 15.1031 12.6031 16 11.5 16H3.5C2.39688 16 1.5 15.1031 1.5 14V4ZM4.5 6C4.225 6 4 6.225 4 6.5V13.5C4 13.775 4.225 14 4.5 14C4.775 14 5 13.775 5 13.5V6.5C5 6.225 4.775 6 4.5 6ZM7.5 6C7.225 6 7 6.225 7 6.5V13.5C7 13.775 7.225 14 7.5 14C7.775 14 8 13.775 8 13.5V6.5C8 6.225 7.775 6 7.5 6ZM10.5 6C10.225 6 10 6.225 10 6.5V13.5C10 13.775 10.225 14 10.5 14C10.775 14 11 13.775 11 13.5V6.5C11 6.225 10.775 6 10.5 6Z"/>
 </svg>

+ 7 - 2
src/components/Button/index.tsx

@@ -10,9 +10,10 @@ type Props = {
   onPress?: () => void;
   containerStyles?: CSSProperties;
   textStyles?: CSSProperties;
+  disabled?: boolean;
 };
 
-export const Button: FC<Props> = ({ children, variant, onPress, containerStyles, textStyles }) => {
+export const Button: FC<Props> = ({ children, variant, onPress, containerStyles, textStyles, disabled }) => {
   return (
     <>
       {variant === ButtonVariants.OPACITY ? (
@@ -21,6 +22,7 @@ export const Button: FC<Props> = ({ children, variant, onPress, containerStyles,
           children={children}
           containerStyles={containerStyles}
           textStyles={textStyles}
+          disabled={disabled}
         />
       ) : variant === ButtonVariants.FILL ? (
         <FillButton
@@ -43,17 +45,20 @@ type VariantProps = {
   onPress?: () => void;
   containerStyles?: CSSProperties | {};
   textStyles?: CSSProperties | {};
+  disabled?: boolean;
 };
 
 const OpacityButton: FC<VariantProps> = ({
   onPress,
   children,
   containerStyles = {},
-  textStyles = {}
+  textStyles = {},
+  disabled = false
 }) => (
   <TouchableOpacity
     style={[styles.button, styles.opacityButton, containerStyles]}
     onPress={onPress}
+    disabled={disabled}
   >
     <Text style={[styles.text, styles.opacityText, textStyles]}>{children}</Text>
   </TouchableOpacity>

+ 19 - 2
src/components/Calendars/RangeCalendar/index.tsx

@@ -6,6 +6,8 @@ import { Modal } from '../../Modal';
 import { styles } from './style';
 import { Colors } from '../../../theme';
 import { Calendar } from 'react-native-calendars';
+import { Button } from 'src/components/Button';
+import { ButtonVariants } from 'src/types/components';
 
 export default function RangeCalendar({
   isModalVisible,
@@ -14,7 +16,7 @@ export default function RangeCalendar({
   disableFutureDates = false,
 }: {
   isModalVisible: boolean;
-  closeModal: (startDate: Date | null, endDate: Date | null) => void;
+  closeModal: (startDate?: Date | null, endDate?: Date | null) => void;
   allowRangeSelection?: boolean;
   disableFutureDates?: boolean;
 }) {
@@ -99,6 +101,12 @@ export default function RangeCalendar({
   })();
 
   const resetSelections = () => {
+    closeModal();
+    setSelectedStartDate(null);
+    setSelectedEndDate(null);
+  };
+
+  const handleClose = () => {
     closeModal(selectedStartDate, selectedEndDate);
     setSelectedStartDate(null);
     setSelectedEndDate(null);
@@ -109,7 +117,7 @@ export default function RangeCalendar({
       visibleInPercent={'auto'}
       visible={isModalVisible}
       onRequestClose={resetSelections}
-      headerTitle="Select Date"
+      headerTitle={allowRangeSelection ? "Select Dates" : "Select Date" }
     >
       <View style={styles.modalContent}>
         <Calendar
@@ -123,6 +131,15 @@ export default function RangeCalendar({
           }}
         />
       </View>
+      <View style={styles.modalFooter}>
+        <Button
+          children="Done"
+          onPress={handleClose}
+          disabled={!selectedStartDate}
+          variant={!selectedStartDate ? ButtonVariants.OPACITY : ButtonVariants.FILL}
+          containerStyles={{ borderWidth: 0 }}
+         />
+      </View>
     </Modal>
   );
 }

+ 7 - 4
src/components/Calendars/RangeCalendar/style.ts

@@ -4,14 +4,17 @@ import { Colors } from '../../../theme';
 export const styles = StyleSheet.create({
   modalContent: {
     backgroundColor: Colors.WHITE,
-    paddingTop: 22,
-    paddingBottom: 52,
-    height: '50%',
+    paddingVertical: 22,
   },
   calendarHeader: {
     flexDirection: 'row',
     justifyContent: 'space-between',
     marginBottom: 16,
-    alignItems: 'center',
+    alignItems: 'center'
   },
+  modalFooter: {
+    justifyContent: 'flex-end',
+    width: '100%',
+    marginBottom: 24,
+  }
 });

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

@@ -23,6 +23,7 @@ type Props = {
   icon?: ReactNode;
   multiline?: boolean;
   editable?: boolean;
+  height?: number;
 };
 
 export const Input: FC<Props> = ({
@@ -37,7 +38,8 @@ export const Input: FC<Props> = ({
   formikError,
   icon,
   multiline,
-  editable
+  editable,
+  height
 }) => {
   const [focused, setFocused] = useState(false);
 
@@ -50,7 +52,7 @@ export const Input: FC<Props> = ({
         style={[
           [styles.wrapper, formikError ? styles.inputError : null],
           { flexDirection: 'row', alignItems: 'center' },
-          multiline ? { height: 100 } : { height: 44 }
+          multiline ? { height: height ?? 100 } : { height: height ?? 44 }
         ]}
       >
         {icon ? (

+ 12 - 0
src/components/WarningModal/index.tsx

@@ -89,6 +89,18 @@ export const WarningModal = ({
           borderColor: Colors.RED
         }
       ]
+    },
+    success: {
+      message,
+      buttons: [
+        {
+          text: 'OK',
+          textColor: Colors.WHITE,
+          color: Colors.ORANGE,
+          action: onClose,
+          borderColor: Colors.ORANGE
+        },
+      ]
     }
   };
 

+ 2 - 1
src/components/WarningModal/styles.tsx

@@ -18,7 +18,8 @@ export const styles = StyleSheet.create({
     },
     shadowOpacity: 0.05,
     shadowRadius: 15,
-    elevation: 5
+    elevation: 5,
+    backgroundColor: Colors.WHITE
   },
   modalContent: {
     display: 'flex',

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

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

+ 7 - 0
src/modules/api/trips/queries/index.ts

@@ -0,0 +1,7 @@
+export * from './use-post-get-trips-years';
+export * from './use-post-get-trips-for-year';
+export * from './use-post-get-regions-for-trips';
+export * from './use-post-delete-trip';
+export * from './use-post-get-trip';
+export * from './use-post-set-new-trip';
+export * from './use-post-set-update-trip';

+ 22 - 0
src/modules/api/trips/queries/use-post-delete-trip.tsx

@@ -0,0 +1,22 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { tripsQueryKeys } from '../trips-query-keys';
+import { tripsApi } from '../trips-api';
+import { ResponseType } from '../../response-type';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const usePostDeleteTripMutation = () => {
+  return useMutation<
+    ResponseType,
+    BaseAxiosError,
+    { token: string; trip_id: number },
+    ResponseType
+  >({
+    mutationKey: tripsQueryKeys.deleteTrip(),
+    mutationFn: async (variables) => {
+      const response = await tripsApi.deleteTrip(variables.token, variables.trip_id);
+      return response.data;
+    }
+  });
+};

+ 17 - 0
src/modules/api/trips/queries/use-post-get-regions-for-trips.tsx

@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { tripsQueryKeys } from '../trips-query-keys';
+import { tripsApi, type PostGetRegionsForTripsReturn } from '../trips-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const useGetRegionsForTripsQuery = (enabled: boolean) => {
+  return useQuery<PostGetRegionsForTripsReturn, BaseAxiosError>({
+    queryKey: tripsQueryKeys.getRegionsForTrips(),
+    queryFn: async () => {
+      const response = await tripsApi.getRegionsForTrips();
+      return response.data;
+    },
+    enabled
+  });
+};

+ 17 - 0
src/modules/api/trips/queries/use-post-get-trip.tsx

@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { tripsQueryKeys } from '../trips-query-keys';
+import { tripsApi, type PostGetTripReturn } from '../trips-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const useGetTripQuery = (token: string, trip_id: number, enabled: boolean) => {
+  return useQuery<PostGetTripReturn, BaseAxiosError>({
+    queryKey: tripsQueryKeys.getTrip(token, trip_id),
+    queryFn: async () => {
+      const response = await tripsApi.getTrip(token, trip_id);
+      return response.data;
+    },
+    enabled
+  });
+};

+ 17 - 0
src/modules/api/trips/queries/use-post-get-trips-for-year.tsx

@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { tripsQueryKeys } from '../trips-query-keys';
+import { tripsApi, type PostGetTripsForYearReturn } from '../trips-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const useGetTripsForYearQuery = (token: string, year: string, enabled: boolean) => {
+  return useQuery<PostGetTripsForYearReturn, BaseAxiosError>({
+    queryKey: tripsQueryKeys.getTripsForYear(token, year),
+    queryFn: async () => {
+      const response = await tripsApi.getTripsForYear(token, year);
+      return response.data;
+    },
+    enabled
+  });
+};

+ 17 - 0
src/modules/api/trips/queries/use-post-get-trips-years.tsx

@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { tripsQueryKeys } from '../trips-query-keys';
+import { tripsApi, type PostGetTripsYearsReturn } from '../trips-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const useGetTripsYearsQuery = (token: string, enabled: boolean) => {
+  return useQuery<PostGetTripsYearsReturn, BaseAxiosError>({
+    queryKey: tripsQueryKeys.getTripsYears(token),
+    queryFn: async () => {
+      const response = await tripsApi.getTripsYears(token);
+      return response.data;
+    },
+    enabled
+  });
+};

+ 16 - 0
src/modules/api/trips/queries/use-post-set-new-trip.tsx

@@ -0,0 +1,16 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { tripsQueryKeys } from '../trips-query-keys';
+import { type PostSetNewTripReturn, type PostSetNewTrip, tripsApi } from '../trips-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const usePostSetNewTripMutation = () => {
+  return useMutation<PostSetNewTripReturn, BaseAxiosError, PostSetNewTrip, PostSetNewTripReturn>({
+    mutationKey: tripsQueryKeys.setNewTrip(),
+    mutationFn: async (data) => {
+      const response = await tripsApi.setNewTrip(data);
+      return response.data;
+    }
+  });
+};

+ 17 - 0
src/modules/api/trips/queries/use-post-set-update-trip.tsx

@@ -0,0 +1,17 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { tripsQueryKeys } from '../trips-query-keys';
+import { type PostUpdateTrip, tripsApi } from '../trips-api';
+import { ResponseType } from '../../response-type';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const usePostUpdateTripMutation = () => {
+  return useMutation<ResponseType, BaseAxiosError, PostUpdateTrip, ResponseType>({
+    mutationKey: tripsQueryKeys.updateTrip(),
+    mutationFn: async (data) => {
+      const response = await tripsApi.updateTrip(data);
+      return response.data;
+    }
+  });
+};

+ 94 - 0
src/modules/api/trips/trips-api.tsx

@@ -0,0 +1,94 @@
+import { request } from '../../../utils';
+import { API } from '../../../types';
+import { ResponseType } from '../response-type';
+
+export interface PostGetTripsYearsReturn extends ResponseType {
+  data: string[];
+}
+
+export interface PostGetTripsForYearReturn extends ResponseType {
+  trips: {
+    id: number;
+    date_from: string;
+    date_to: string;
+    description: string;
+    regions: {
+      region_name: string;
+      flag1: string;
+      flag2: string | null;
+      status: 0 | 1;
+      id: number;
+    }[];
+  }[];
+}
+
+export interface RegionData {
+  id: number;
+  quality: number;
+  status: 0 | 1;
+  hidden: boolean;
+}
+
+export interface PostSetNewTrip {
+  token: string;
+  date_from: string;
+  date_to: string;
+  description: string;
+  regions: RegionData[];
+}
+
+export interface PostSetNewTripReturn extends ResponseType {
+  trip_id: number;
+}
+
+export interface PostUpdateTrip {
+  token: string;
+  trip_id: number;
+  date_from: string;
+  date_to: string;
+  description: string;
+  regions: RegionData[];
+}
+
+export interface PostGetTripReturn extends ResponseType {
+  trip: {
+    id: number;
+    date_from: string;
+    date_to: string;
+    description: string;
+    regions: {
+      id: number;
+      region_name: string;
+      flag1: string;
+      flag2: string | null;
+      quality: number;
+      status: 0 | 1;
+      hidden: boolean;
+    }[];
+  };
+}
+
+export interface PostGetRegionsForTripsReturn extends ResponseType {
+  regions: {
+    id: number;
+    region_name: string;
+    flag1: string;
+    flag2: string | null;
+    hidden: boolean;
+  }[];
+}
+
+export const tripsApi = {
+  getTripsYears: (token: string) =>
+    request.postForm<PostGetTripsYearsReturn>(API.GET_TRIPS_YEARS, { token }),
+  getTripsForYear: (token: string, year: string) =>
+    request.postForm<PostGetTripsForYearReturn>(API.GET_TRIPS_FOR_YEAR, { token, year }),
+  setNewTrip: (data: PostSetNewTrip) =>
+    request.postForm<PostSetNewTripReturn>(API.SET_NEW_TRIP, data),
+  updateTrip: (data: PostUpdateTrip) => request.postForm<ResponseType>(API.UPDATE_TRIP, data),
+  deleteTrip: (token: string, trip_id: number) =>
+    request.postForm<ResponseType>(API.DELETE_TRIP, { token, trip_id }),
+  getTrip: (token: string, trip_id: number) =>
+    request.postForm<PostGetTripReturn>(API.GET_TRIP, { token, trip_id }),
+  getRegionsForTrips: () => request.get<PostGetRegionsForTripsReturn>(API.GET_REGIONS_FOR_TRIPS)
+};

+ 9 - 0
src/modules/api/trips/trips-query-keys.tsx

@@ -0,0 +1,9 @@
+export const tripsQueryKeys = {
+  getTripsYears: (token: string) => ['getTripsYears', { token }] as const,
+  getTripsForYear: (token: string, year: string) => ['getTripsForYear', { token, year }] as const,
+  setNewTrip: () => ['setNewTrip'] as const,
+  updateTrip: () => ['updateTrip'] as const,
+  deleteTrip: () => ['deleteTrip'] as const,
+  getTrip: (token: string, trip_id: number) => ['getTrip', { token, trip_id }] as const,
+  getRegionsForTrips: () => ['getRegionsForTrips'] as const
+};

+ 344 - 0
src/screens/InAppScreens/TravelsScreen/AddNewTripScreen/index.tsx

@@ -0,0 +1,344 @@
+import React, { useEffect, useState } from 'react';
+import { View, Text, TouchableOpacity, ScrollView } from 'react-native';
+import ReactModal from 'react-native-modal';
+import { useNavigation } from '@react-navigation/native';
+
+import { PageWrapper, Header, Input, WarningModal } from 'src/components';
+import RegionItem from '../Components/RegionItem';
+import RangeCalendar from 'src/components/Calendars/RangeCalendar';
+
+import { StoreType, storage } from 'src/storage';
+import { Colors } from 'src/theme';
+import { NAVIGATION_PAGES } from 'src/types';
+import { RegionAddData } from '../utils/types';
+import {
+  useGetTripQuery,
+  usePostDeleteTripMutation,
+  usePostUpdateTripMutation,
+  usePostSetNewTripMutation
+} from '@api/trips';
+import { qualityOptions } from '../utils/constants';
+import { styles } from './styles';
+
+import CalendarSvg from '../../../../../assets/icons/calendar.svg';
+
+const AddNewTripScreen = ({ route }: { route: any }) => {
+  const editTripId = route.params?.editTripId ?? null;
+  const token = storage.get('token', StoreType.STRING) as string;
+  const { data: editData } = useGetTripQuery(token, editTripId, Boolean(editTripId));
+  const navigation = useNavigation();
+  const [calendarVisible, setCalendarVisible] = useState(false);
+  const [selectedDates, setSelectedDates] = useState<string | null>(null);
+  const [description, setDescription] = useState<string>('');
+  const [regions, setRegions] = useState<RegionAddData[] | null>(null);
+  const [disabled, setDisabled] = useState(true);
+  const [qualitySelectorVisible, setQualitySelectorVisible] = useState(false);
+  const [selectedRegionId, setSelectedRegionId] = useState<number | null>(null);
+  const [isWarningModalVisible, setIsWarningModalVisible] = useState(false);
+
+  const { mutate: saveNewTrip } = usePostSetNewTripMutation();
+  const { mutate: updateTrip } = usePostUpdateTripMutation();
+  const { mutate: deleteTrip } = usePostDeleteTripMutation();
+
+  useEffect(() => {
+    if (route.params?.regionsToSave) {
+      setRegions((currentRegions) => {
+        const newRegionsIds = route.params.regionsToSave.map((region: RegionAddData) => region.id);
+        const existingRegions = currentRegions?.filter((region) =>
+          newRegionsIds.includes(region.id)
+        );
+
+        const updatedRegions = route.params.regionsToSave.map((newRegion: RegionAddData) => {
+          const existingRegion = existingRegions?.find((region) => region.id === newRegion.id);
+
+          return {
+            ...newRegion,
+            quality: existingRegion ? existingRegion.quality : 3,
+            status: existingRegion ? existingRegion.status : 0
+          };
+        });
+
+        return updatedRegions;
+      });
+    }
+  }, [route.params?.regionsToSave]);
+
+  function extractNumberAndExtension(path: string | null) {
+    if (!path) return null;
+    const slashIndex = path.lastIndexOf('/');
+    return path.substring(slashIndex + 1);
+  }
+
+  useEffect(() => {
+    if (editData && editData.trip) {
+      setSelectedDates(editData.trip.date_from + ' - ' + editData.trip.date_to);
+      setDescription(editData.trip.description);
+      setRegions(
+        editData.trip.regions.map((region: any) => {
+          return {
+            ...region,
+            flag1: extractNumberAndExtension(region.flag1),
+            flag2: extractNumberAndExtension(region.flag2)
+          };
+        })
+      );
+    }
+  }, [editData]);
+
+  useEffect(() => {
+    if (regions?.length && selectedDates) {
+      setDisabled(false);
+    } else {
+      setDisabled(true);
+    }
+  }, [regions, selectedDates]);
+
+  const changeQualityForRegion = (regionId: number | null, newQuality: number) => {
+    regions &&
+      setRegions(
+        regions.map((region) => {
+          if (region.id === regionId) {
+            return { ...region, quality: newQuality };
+          }
+          return region;
+        })
+      );
+  };
+
+  const changeStatusForRegion = (regionId: number | null) => {
+    regions &&
+      setRegions(
+        regions.map((region) => {
+          if (region.id === regionId) {
+            return { ...region, status: region.status === 1 ? 0 : 1 };
+          }
+          return region;
+        })
+      );
+  };
+
+  const handleDeleteRegion = (regionId: number) => {
+    regions && setRegions(regions.filter((region) => region.id !== regionId));
+  };
+
+  const handleDeleteTrip = () => {
+    deleteTrip(
+      {
+        token,
+        trip_id: editTripId
+      },
+      {
+        onSuccess: () => {
+          setIsWarningModalVisible(false);
+          navigation.navigate(...([NAVIGATION_PAGES.TRIPS, { deleted: true }] as never));
+        }
+      }
+    );
+  };
+
+  const handleSaveNewTrip = () => {
+    if (regions && selectedDates) {
+      const isStartDateInFuture =
+        selectedDates.split(' - ')[0] > new Date().toISOString().split('T')[0];
+      const regionsData = regions.map((region) => {
+        return {
+          id: region.id,
+          quality: region.quality ?? 3,
+          status: isStartDateInFuture ? 0 : (Number(region.status) as 0 | 1),
+          hidden: region.hidden
+        };
+      });
+
+      saveNewTrip(
+        {
+          token,
+          date_from: selectedDates.split(' - ')[0],
+          date_to: selectedDates.split(' - ')[1],
+          description,
+          regions: regionsData
+        },
+        {
+          onSuccess: () => {
+            navigation.navigate(...([NAVIGATION_PAGES.TRIPS, { saved: true }] as never));
+          }
+        }
+      );
+    }
+  };
+
+  const handleUpdateTrip = () => {
+    if (regions && selectedDates) {
+      const isStartDateInFuture =
+        selectedDates.split(' - ')[0] > new Date().toISOString().split('T')[0];
+      const regionsData = regions.map((region) => {
+        return {
+          id: region.id,
+          quality: region.quality ?? 3,
+          status: isStartDateInFuture ? 0 : (Number(region.status) as 0 | 1),
+          hidden: region.hidden
+        };
+      });
+
+      updateTrip(
+        {
+          token,
+          trip_id: editTripId,
+          date_from: selectedDates.split(' - ')[0],
+          date_to: selectedDates.split(' - ')[1],
+          description,
+          regions: regionsData
+        },
+        {
+          onSuccess: (res) => {
+            navigation.navigate(...([NAVIGATION_PAGES.TRIPS, { updated: true }] as never));
+          }
+        }
+      );
+    }
+  };
+
+  return (
+    <PageWrapper style={{ flex: 1 }}>
+      <Header label={editTripId ? 'Edit Trip' : 'Add New Trip'} />
+      <ScrollView
+        contentContainerStyle={{ flexGrow: 1, gap: 16 }}
+        showsVerticalScrollIndicator={false}
+      >
+        <TouchableOpacity style={styles.regionSelector} onPress={() => setCalendarVisible(true)}>
+          <CalendarSvg />
+          <Text style={styles.regionText}>{selectedDates ?? 'Add dates'}</Text>
+        </TouchableOpacity>
+
+        <Input
+          placeholder="Add description and all interesting moments of your trip"
+          inputMode={'text'}
+          onChange={(text) => setDescription(text)}
+          value={description}
+          header="Description"
+          height={54}
+          multiline={true}
+        />
+
+        <View style={{ marginBottom: 8 }}>
+          <Text style={styles.regionsLabel}>Regions</Text>
+          <TouchableOpacity
+            style={styles.addRegionBtn}
+            onPress={() =>
+              navigation.navigate(
+                ...([
+                  NAVIGATION_PAGES.ADD_REGIONS,
+                  { regionsParams: regions, editId: editTripId }
+                ] as never)
+              )
+            }
+          >
+            <Text style={styles.addRegionBtntext}>Add Region</Text>
+          </TouchableOpacity>
+          {regions && regions.length ? (
+            <View style={styles.regionsContainer}>
+              {regions.map((region) => {
+                return (
+                  <RegionItem
+                    key={region.id}
+                    region={region}
+                    onDelete={() => handleDeleteRegion(region.id)}
+                    onToggleStatus={() => changeStatusForRegion(region.id)}
+                    onQualityChange={() => {
+                      setSelectedRegionId(region.id);
+                      setQualitySelectorVisible(true);
+                    }}
+                    startDate={selectedDates ? selectedDates.split(' - ')[0] : null}
+                  />
+                );
+              })}
+            </View>
+          ) : (
+            <Text style={styles.noRegiosText}>No regions at the moment</Text>
+          )}
+        </View>
+      </ScrollView>
+
+      <View style={styles.tabContainer}>
+        {editTripId ? (
+          <>
+            <TouchableOpacity
+              style={[styles.tabStyle, styles.deleteTab]}
+              onPress={() => setIsWarningModalVisible(true)}
+            >
+              <Text style={[styles.tabText, styles.deleteTabText]}>Delete Trip</Text>
+            </TouchableOpacity>
+            <TouchableOpacity
+              style={[
+                styles.tabStyle,
+                styles.addNewTab,
+                disabled && { backgroundColor: Colors.LIGHT_GRAY, borderColor: Colors.LIGHT_GRAY }
+              ]}
+              onPress={handleUpdateTrip}
+              disabled={disabled}
+            >
+              <Text style={[styles.tabText, styles.addNewTabText]}>Save Trip</Text>
+            </TouchableOpacity>
+          </>
+        ) : (
+          <TouchableOpacity
+            style={[
+              styles.tabStyle,
+              styles.addNewTab,
+              disabled && { backgroundColor: Colors.LIGHT_GRAY, borderColor: Colors.LIGHT_GRAY },
+              { paddingVertical: 12 }
+            ]}
+            onPress={handleSaveNewTrip}
+            disabled={disabled}
+          >
+            <Text style={[styles.tabText, styles.addNewTabText]}>Add New Trip</Text>
+          </TouchableOpacity>
+        )}
+      </View>
+
+      <RangeCalendar
+        isModalVisible={calendarVisible}
+        closeModal={(startDate?: Date | null, endDate?: Date | null) => {
+          startDate &&
+            setSelectedDates(
+              startDate.toString() + ' - ' + (endDate ? endDate?.toString() : startDate?.toString())
+            );
+          setCalendarVisible(false);
+        }}
+      />
+      <ReactModal
+        isVisible={qualitySelectorVisible}
+        onBackdropPress={() => setQualitySelectorVisible(false)}
+        style={styles.modal}
+        statusBarTranslucent={true}
+        presentationStyle="overFullScreen"
+      >
+        <View style={styles.wrapper}>
+          <View style={{ paddingBottom: 16 }}>
+            {qualityOptions.map((option) => (
+              <TouchableOpacity
+                key={option.id}
+                style={styles.btnOption}
+                onPress={() => {
+                  setQualitySelectorVisible(false);
+                  changeQualityForRegion(selectedRegionId, option.id);
+                }}
+              >
+                <Text style={styles.btnOptionText}>{option.name}</Text>
+              </TouchableOpacity>
+            ))}
+          </View>
+        </View>
+      </ReactModal>
+      <WarningModal
+        type={'delete'}
+        isVisible={isWarningModalVisible}
+        onClose={() => setIsWarningModalVisible(false)}
+        title="Delete Trip"
+        message="Are you sure you want to delete your trip?"
+        action={handleDeleteTrip}
+      />
+    </PageWrapper>
+  );
+};
+
+export default AddNewTripScreen;

+ 100 - 0
src/screens/InAppScreens/TravelsScreen/AddNewTripScreen/styles.tsx

@@ -0,0 +1,100 @@
+import { StyleSheet } from 'react-native';
+import { Colors } from 'src/theme';
+import { getFontSize } from 'src/utils';
+
+export const styles = StyleSheet.create({
+  tabContainer: { flexDirection: 'row', gap: 16, alignItems: 'center', marginVertical: 8 },
+  tabStyle: {
+    flex: 1,
+    borderRadius: 4,
+    alignItems: 'center',
+    justifyContent: 'center',
+    paddingHorizontal: 16,
+    paddingVertical: 8,
+    gap: 4,
+    borderWidth: 1
+  },
+  deleteTab: {
+    backgroundColor: 'transparent',
+    borderColor: Colors.RED
+  },
+  tabText: {
+    fontSize: getFontSize(14),
+    fontWeight: 'bold',
+    fontFamily: 'redhat-700'
+  },
+  deleteTabText: {
+    color: Colors.RED
+  },
+  addNewTab: {
+    backgroundColor: Colors.ORANGE,
+    borderColor: Colors.ORANGE
+  },
+  addNewTabText: { color: Colors.WHITE },
+  regionSelector: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingHorizontal: 16,
+    borderRadius: 4,
+    height: 36,
+    backgroundColor: 'rgba(250, 250, 250, 1)',
+    justifyContent: 'flex-start',
+    gap: 10
+  },
+  regionText: {
+    fontSize: 15,
+    color: Colors.LIGHT_GRAY
+  },
+  regionsLabel: {
+    color: Colors.DARK_BLUE,
+    fontSize: getFontSize(14),
+    fontFamily: 'redhat-700',
+    marginBottom: 5
+  },
+  noRegiosText: {
+    fontSize: 14,
+    fontWeight: '500',
+    color: Colors.LIGHT_GRAY,
+    textAlign: 'center',
+    paddingVertical: 8
+  },
+  addRegionBtn: {
+    display: 'flex',
+    justifyContent: 'center',
+    alignItems: 'center',
+    borderRadius: 4,
+    gap: 10,
+    padding: 10,
+    borderColor: Colors.DARK_BLUE,
+    borderWidth: 1,
+    borderStyle: 'solid',
+    marginBottom: 16
+  },
+  addRegionBtntext: {
+    color: Colors.DARK_BLUE,
+    fontSize: getFontSize(14),
+    fontFamily: 'redhat-700'
+  },
+  regionsContainer: {
+    gap: 12
+  },
+  btnOption: {
+    paddingHorizontal: 16,
+    paddingVertical: 9,
+    flexDirection: 'row',
+    alignItems: 'center',
+    gap: 16
+  },
+  btnOptionText: { fontSize: 16, fontWeight: '600', color: Colors.DARK_BLUE },
+  wrapper: {
+    backgroundColor: Colors.WHITE,
+    padding: 16,
+    borderTopLeftRadius: 10,
+    borderTopRightRadius: 10,
+    height: 'auto'
+  },
+  modal: {
+    justifyContent: 'flex-end',
+    margin: 0
+  }
+});

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

@@ -297,7 +297,7 @@ const AddPhotoScreen = ({ route }: { route: any }) => {
 
       <RangeCalendar
         isModalVisible={calendarVisible}
-        closeModal={(startDate: Date | null, endDate: Date | null) => {
+        closeModal={(startDate?: Date | null, endDate?: Date | null) => {
           startDate && setSelectedDate(startDate.toString());
           setCalendarVisible(false);
         }}

+ 282 - 0
src/screens/InAppScreens/TravelsScreen/AddRegionsScreen/index.tsx

@@ -0,0 +1,282 @@
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
+import { SafeAreaView, View, Text, Platform, TouchableOpacity } from 'react-native';
+import MapView, { Geojson, UrlTile } from 'react-native-maps';
+import * as turf from '@turf/turf';
+import { Feature } from '@turf/turf';
+import { useNavigation } from '@react-navigation/native';
+
+import { Header, Modal, FlatList as List } from 'src/components';
+
+import { MAP_HOST } from 'src/constants';
+import { Colors } from 'src/theme';
+import { findRegionInDataset } from 'src/utils/mapHelpers';
+import { calculateMapRegion } from '../utils/calculateRegion';
+import { FeatureCollection } from 'src/types/map';
+import { NAVIGATION_PAGES } from 'src/types';
+import { RegionAddData } from '../utils/types';
+import { useGetRegionsForTripsQuery } from '@api/trips';
+import { styles } from './styles';
+
+import regionsGeojson from '../../../../../assets/geojson/nm2022.json';
+import SearchSvg from '../../../../../assets/icons/search.svg';
+import SaveSvg from '../../../../../assets/icons/travels-screens/save.svg';
+
+const AddRegionsScreen = ({ route }: { route: any }) => {
+  const { regionsParams }: { regionsParams: RegionAddData[] } = route.params;
+  const { data } = useGetRegionsForTripsQuery(true);
+  const navigation = useNavigation();
+
+  const [regions, setRegions] = useState<RegionAddData[] | null>(null);
+  const [isModalVisible, setIsModalVisible] = useState(false);
+  const [selectedRegions, setSelectedRegions] = useState<FeatureCollection[]>([]);
+  const [regionsToSave, setRegionsToSave] = useState<RegionAddData[]>([]);
+  const [regionData, setRegionData] = useState<RegionAddData | null>(null);
+  const [regionPopupVisible, setRegionPopupVisible] = useState(false);
+  const mapRef = useRef<MapView>(null);
+
+  useEffect(() => {
+    if (data && data.regions) {
+      setRegions(data.regions);
+    }
+  }, [data]);
+
+  useEffect(() => {
+    const addRegionsAsync = async () => {
+      if (regionsParams) {
+        const promises = regionsParams.map((param) => {
+          const foundRegion: Feature | undefined = (
+            regionsGeojson as FeatureCollection
+          ).features.find((region) => region.properties?.id === param.id);
+          if (foundRegion) {
+            setRegionsToSave((prevRegions) => [...prevRegions, param]);
+            return {
+              type: 'FeatureCollection',
+              features: [
+                {
+                  geometry: foundRegion.geometry,
+                  properties: foundRegion.properties,
+                  type: 'Feature'
+                }
+              ]
+            };
+          }
+          return null;
+        });
+
+        const results = await Promise.all(promises);
+        const validRegions = results.filter(Boolean);
+
+        setSelectedRegions(
+          (prevSelectedRegions) => [...prevSelectedRegions, ...validRegions] as FeatureCollection[]
+        );
+      }
+    };
+
+    addRegionsAsync();
+  }, [regionsParams]);
+
+  const addRegionFromSearch = async (searchRegion: RegionAddData) => {
+    const foundRegion = (regionsGeojson as FeatureCollection).features.find(
+      (region) => region.properties?.id === searchRegion.id
+    );
+
+    if (foundRegion) {
+      const regionIndex = selectedRegions.findIndex(
+        (region) => region.features[0].properties?.id === searchRegion.id
+      );
+      const regionFromApi = regions?.find((region) => region.id === searchRegion.id);
+
+      if (regionIndex < 0 && regionFromApi) {
+        const newRegion = {
+          type: 'FeatureCollection',
+          features: [
+            {
+              geometry: foundRegion.geometry,
+              properties: foundRegion.properties,
+              type: 'Feature'
+            }
+          ]
+        };
+
+        setSelectedRegions([...selectedRegions, newRegion as FeatureCollection]);
+        setRegionsToSave((prevRegions) => [...prevRegions, regionFromApi]);
+        setRegionPopupVisible(true);
+
+        const bounds = turf.bbox(foundRegion);
+        const region = calculateMapRegion(bounds);
+        mapRef.current?.animateToRegion(region, 1000);
+      }
+    }
+  };
+
+  const handleSavePress = () => {
+    navigation.navigate(
+      ...([
+        NAVIGATION_PAGES.ADD_TRIP,
+        { regionsToSave: regionsToSave, editTripId: route.params?.editId }
+      ] as never)
+    );
+  };
+
+  const handleSetRegionData = (regionId: number) => {
+    const foundRegion = regions?.find((region) => region.id === regionId);
+
+    if (foundRegion) {
+      setRegionData(foundRegion);
+      setRegionsToSave((prevRegions) => [...prevRegions, foundRegion]);
+    }
+  };
+
+  const handleMapPress = useCallback(
+    async (event: {
+      nativeEvent: { coordinate: { latitude: any; longitude: any }; action?: string };
+    }) => {
+      if (event.nativeEvent?.action === 'polygon-press') return;
+
+      const { latitude, longitude } = event.nativeEvent.coordinate;
+      const point = turf.point([longitude, latitude]);
+
+      let foundRegion = findRegionInDataset(regionsGeojson, point);
+
+      if (foundRegion) {
+        const id = foundRegion.properties?.id;
+
+        const newRegion = {
+          type: 'FeatureCollection',
+          features: [
+            {
+              geometry: foundRegion.geometry,
+              properties: foundRegion.properties,
+              type: 'Feature'
+            }
+          ]
+        };
+
+        const regionIndex = selectedRegions.findIndex(
+          (region) => region.features[0].properties?.id === id
+        );
+
+        if (regionIndex >= 0) {
+          const newSelectedRegions = [...selectedRegions];
+          newSelectedRegions.splice(regionIndex, 1);
+          setSelectedRegions(newSelectedRegions);
+          setRegionsToSave(regionsToSave.filter((region) => region.id !== id));
+          setRegionPopupVisible(false);
+          return;
+        } else {
+          setSelectedRegions([...selectedRegions, newRegion] as FeatureCollection[]);
+        }
+
+        handleSetRegionData(id);
+        setRegionPopupVisible(true);
+
+        const bounds = turf.bbox(foundRegion);
+        const region = calculateMapRegion(bounds);
+
+        mapRef.current?.animateToRegion(region, 1000);
+      }
+    },
+    [selectedRegions, regions]
+  );
+
+  function renderGeoJSON() {
+    if (!selectedRegions || !selectedRegions.length) return null;
+
+    return selectedRegions.map((region, index) => (
+      <Geojson
+        key={index}
+        geojson={region as any}
+        fillColor="rgba(237, 147, 52, 0.5)"
+        strokeColor={Colors.ORANGE}
+        strokeWidth={Platform.OS == 'android' ? 2 : 1}
+        zIndex={3}
+        tracksViewChanges={false}
+      />
+    ));
+  }
+
+  const renderedGeoJSON = useMemo(() => renderGeoJSON(), [selectedRegions]);
+
+  return (
+    <SafeAreaView style={{ height: '100%' }}>
+      <View style={styles.wrapper}>
+        <Header label={'Add Regions'} />
+        <View style={styles.searchContainer}>
+          <TouchableOpacity style={[styles.regionSelector]} onPress={() => setIsModalVisible(true)}>
+            <SearchSvg fill={Colors.LIGHT_GRAY} />
+            <Text style={styles.regionText}>Search</Text>
+          </TouchableOpacity>
+          <TouchableOpacity
+            style={[
+              styles.saveBtn,
+              selectedRegions.length ? styles.saveBtnActive : styles.saveBtnDisabled
+            ]}
+            onPress={handleSavePress}
+            disabled={!selectedRegions.length}
+          >
+            <SaveSvg fill={selectedRegions.length ? Colors.WHITE : Colors.LIGHT_GRAY} />
+          </TouchableOpacity>
+        </View>
+      </View>
+
+      <View style={styles.container}>
+        <MapView
+          ref={mapRef}
+          style={styles.map}
+          showsMyLocationButton={false}
+          showsCompass={false}
+          zoomControlEnabled={false}
+          mapType={Platform.OS == 'android' ? 'none' : 'standard'}
+          maxZoomLevel={15}
+          minZoomLevel={0}
+          initialRegion={{
+            latitude: 0,
+            longitude: 0,
+            latitudeDelta: 180,
+            longitudeDelta: 180
+          }}
+          onPress={handleMapPress}
+        >
+          <UrlTile
+            urlTemplate={`${MAP_HOST}/tiles_osm/{z}/{x}/{y}`}
+            maximumZ={15}
+            maximumNativeZ={13}
+            shouldReplaceMapContent
+            minimumZ={0}
+          />
+          <UrlTile
+            urlTemplate={`${MAP_HOST}/tiles_nm/grid/{z}/{x}/{y}`}
+            maximumZ={15}
+            maximumNativeZ={13}
+            shouldReplaceMapContent
+            minimumZ={0}
+            opacity={0.3}
+          />
+          {renderedGeoJSON}
+        </MapView>
+      </View>
+      {regionPopupVisible && regionData && (
+        <View style={styles.popupWrapper}>
+          <View style={styles.popupContainer}>
+            <Text style={styles.popupText}>{regionData.name ?? regionData.region_name}</Text>
+          </View>
+        </View>
+      )}
+      <Modal
+        onRequestClose={() => setIsModalVisible(false)}
+        headerTitle={'Select Regions'}
+        visible={isModalVisible}
+      >
+        <List
+          itemObject={(object) => {
+            setIsModalVisible(false);
+            setRegionData(object);
+            addRegionFromSearch(object);
+          }}
+        />
+      </Modal>
+    </SafeAreaView>
+  );
+};
+
+export default AddRegionsScreen;

+ 73 - 0
src/screens/InAppScreens/TravelsScreen/AddRegionsScreen/styles.tsx

@@ -0,0 +1,73 @@
+import { StyleSheet } from 'react-native';
+import { Colors } from 'src/theme';
+import { getFontSize } from 'src/utils';
+
+export const styles = StyleSheet.create({
+  container: {
+    flex: 1
+  },
+  wrapper: {
+    marginLeft: '5%',
+    marginRight: '5%',
+    alignItems: 'center'
+  },
+  map: {
+    ...StyleSheet.absoluteFillObject
+  },
+  searchContainer: {
+    gap: 16,
+    flexDirection: 'row',
+    alignItems: 'center',
+    marginBottom: 8
+  },
+  regionSelector: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingHorizontal: 16,
+    borderRadius: 4,
+    height: 36,
+    backgroundColor: 'rgba(250, 250, 250, 1)',
+    justifyContent: 'flex-start',
+    gap: 10,
+    flex: 1
+  },
+  regionText: {
+    fontSize: 15,
+    fontWeight: '500',
+    color: Colors.LIGHT_GRAY
+  },
+  saveBtn: {
+    borderRadius: 20,
+    paddingVertical: 10,
+    paddingHorizontal: 16,
+    borderWidth: 1
+  },
+  saveBtnActive: {
+    borderColor: Colors.ORANGE,
+    backgroundColor: Colors.ORANGE
+  },
+  saveBtnDisabled: {
+    borderColor: Colors.LIGHT_GRAY
+  },
+  popupWrapper: {
+    marginHorizontal: 24,
+    position: 'absolute',
+    bottom: 25,
+    left: 0,
+    right: 0
+  },
+  popupContainer: {
+    alignItems: 'center',
+    justifyContent: 'center',
+    alignSelf: 'center',
+    paddingVertical: 8,
+    paddingHorizontal: 16,
+    backgroundColor: 'rgba(33, 37, 41, 0.8)',
+    borderRadius: 100
+  },
+  popupText: {
+    color: Colors.WHITE,
+    fontSize: getFontSize(12),
+    fontWeight: '600'
+  }
+});

+ 95 - 0
src/screens/InAppScreens/TravelsScreen/Components/RegionItem/index.tsx

@@ -0,0 +1,95 @@
+import React from 'react';
+import { View, Text, Image, TouchableOpacity } from 'react-native';
+
+import { qualityOptions } from '../../utils/constants';
+import { RegionAddData } from '../../utils/types';
+import { Colors } from 'src/theme';
+import { API_HOST } from 'src/constants';
+import { styles } from './styles';
+
+import MarkIcon from 'assets/icons/mark.svg';
+import TrashSvg from 'assets/icons/travels-screens/trash-solid.svg';
+import ChevronIcon from 'assets/icons/travels-screens/down-arrow.svg';
+
+const RegionItem = ({
+  region,
+  onDelete,
+  onToggleStatus,
+  onQualityChange,
+  startDate
+}: {
+  region: RegionAddData;
+  onDelete: () => void;
+  onToggleStatus: () => void;
+  onQualityChange: () => void;
+  startDate: string | null;
+}) => {
+  const name = region.region_name.split(/ – | - /);
+  const qualityName = region.quality
+    ? qualityOptions.find((q) => q.id === region.quality)?.name
+    : 'Good Visit';
+  const disabled = !startDate || startDate > new Date().toISOString().split('T')[0];
+
+  return (
+    <View key={region.id} style={styles.regionItem}>
+      <View style={styles.regionItemTop}>
+        <View style={styles.regionHeader}>
+          <Image
+            source={{ uri: `${API_HOST}/img/flags_new/${region.flag1}` }}
+            style={styles.flagStyle}
+          />
+          {region.flag2 && (
+            <Image
+              source={{ uri: `${API_HOST}/img/flags_new/${region.flag2}` }}
+              style={[styles.flagStyle, { marginLeft: -17 }]}
+            />
+          )}
+          <View style={styles.nameContainer}>
+            <Text style={styles.regionName}>{name[0]}</Text>
+            <Text style={styles.regionSubname}>{name[1]}</Text>
+          </View>
+        </View>
+        <TouchableOpacity style={styles.trashBtn} onPress={onDelete}>
+          <TrashSvg fill={Colors.WHITE} />
+        </TouchableOpacity>
+      </View>
+
+      <View style={styles.divider}></View>
+
+      <Text style={styles.qualityOfVisit}>Quality of visit</Text>
+      <View style={styles.regionItemBottom}>
+        <TouchableOpacity style={styles.qualitySelector} onPress={onQualityChange}>
+          <Text style={styles.qualityButtonText}>{qualityName}</Text>
+          <ChevronIcon width={18} height={18} />
+        </TouchableOpacity>
+
+        <TouchableOpacity
+          style={
+            region.status === 1 && !disabled
+              ? styles.markNotCompletedButton
+              : [
+                  styles.markCompletedButton,
+                  disabled && {
+                    backgroundColor: Colors.LIGHT_GRAY,
+                    borderColor: Colors.LIGHT_GRAY
+                  }
+                ]
+          }
+          onPress={onToggleStatus}
+          disabled={disabled}
+        >
+          {region.status === 1 && !disabled ? (
+            <View style={styles.completedContainer}>
+              <MarkIcon width={16} height={16} />
+              <Text style={styles.markNotCompletedButtonText}>Completed</Text>
+            </View>
+          ) : (
+            <Text style={[styles.markCompletedButtonText]}>Mark Completed</Text>
+          )}
+        </TouchableOpacity>
+      </View>
+    </View>
+  );
+};
+
+export default RegionItem;

+ 101 - 0
src/screens/InAppScreens/TravelsScreen/Components/RegionItem/styles.tsx

@@ -0,0 +1,101 @@
+import { StyleSheet } from 'react-native';
+import { Colors } from 'src/theme';
+
+export const styles = StyleSheet.create({
+  regionItem: {
+    paddingVertical: 12,
+    paddingHorizontal: 16,
+    borderRadius: 8,
+    backgroundColor: '#FAFAFA'
+  },
+  regionItemTop: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between'
+  },
+  regionHeader: { flexDirection: 'row', alignItems: 'center', flex: 1 },
+  flagStyle: {
+    width: 32,
+    height: 32,
+    borderRadius: 16,
+    borderWidth: 1,
+    borderColor: Colors.LIGHT_GRAY,
+    marginRight: 12
+  },
+  nameContainer: {
+    flexShrink: 1
+  },
+  regionName: {
+    fontSize: 14,
+    fontWeight: 'bold',
+    color: Colors.DARK_BLUE
+  },
+  regionSubname: {
+    fontSize: 12,
+    fontWeight: '400',
+    color: Colors.DARK_BLUE
+  },
+  regionItemBottom: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    gap: 16
+  },
+  qualityOfVisit: {
+    fontSize: 12,
+    color: Colors.DARK_BLUE,
+    fontWeight: '600',
+    marginBottom: 8
+  },
+  markCompletedButton: {
+    backgroundColor: Colors.ORANGE,
+    paddingVertical: 8,
+    paddingHorizontal: 12,
+    borderRadius: 6,
+    flex: 1,
+    alignItems: 'center',
+    borderWidth: 1,
+    borderColor: Colors.ORANGE
+  },
+  markNotCompletedButton: {
+    backgroundColor: 'transparent',
+    paddingVertical: 8,
+    paddingHorizontal: 12,
+    borderRadius: 6,
+    alignItems: 'center',
+    borderColor: Colors.BORDER_LIGHT,
+    borderWidth: 1,
+    flex: 1
+  },
+  markCompletedButtonText: {
+    color: 'white',
+    fontWeight: 'bold',
+    fontSize: 13
+  },
+  markNotCompletedButtonText: {
+    color: Colors.DARK_BLUE,
+    fontWeight: 'bold',
+    fontSize: 13
+  },
+  trashBtn: {
+    backgroundColor: Colors.RED,
+    padding: 8,
+    borderRadius: 50
+  },
+  qualitySelector: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingHorizontal: 12,
+    paddingVertical: 8,
+    borderRadius: 6,
+    backgroundColor: Colors.DARK_LIGHT,
+    justifyContent: 'space-between',
+    flex: 1
+  },
+  qualityButtonText: {
+    color: Colors.DARK_BLUE,
+    fontWeight: 'bold',
+    fontSize: 13
+  },
+  divider: { height: 1, backgroundColor: Colors.DARK_LIGHT, marginVertical: 16 },
+  completedContainer: { gap: 8, flexDirection: 'row', alignItems: 'center' }
+});

+ 91 - 0
src/screens/InAppScreens/TravelsScreen/Components/TripItem/index.tsx

@@ -0,0 +1,91 @@
+import React from 'react';
+import { View, Text, TouchableOpacity, ScrollView, Image } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+import moment from 'moment';
+
+import { TripsData } from '../../utils/types';
+import { API_HOST } from 'src/constants';
+import { Colors } from 'src/theme';
+import { NAVIGATION_PAGES } from 'src/types';
+import { styles } from './styles';
+
+import CalendarIcon from 'assets/icons/travels-screens/calendar.svg';
+import EditIcon from 'assets/icons/travels-screens/pen-to-square.svg';
+
+const TripItem = ({ item }: { item: TripsData }) => {
+  const navigation = useNavigation();
+
+  const formatDate = (dateString: string) => {
+    const date = moment(dateString);
+    const formattedDate = date.format('MMM DD');
+    const year = date.format('YYYY');
+
+    return (
+      <Text style={styles.alignCenter}>
+        <Text style={styles.tripDateText}>{formattedDate}</Text>
+        {'\n'}
+        <Text style={styles.tripDateText}>{year}</Text>
+      </Text>
+    );
+  };
+  return (
+    <View style={styles.tripItemContainer}>
+      <View style={styles.tripHeaderContainer}>
+        <View style={styles.tripDateContainer}>
+          <CalendarIcon fill={Colors.DARK_BLUE} />
+          <Text style={[styles.tripDateText, { marginLeft: 8 }]}>{formatDate(item.date_from)}</Text>
+          <Text style={styles.tripDateText}>-</Text>
+          <Text style={styles.tripDateText}>{formatDate(item.date_to)}</Text>
+        </View>
+        <TouchableOpacity
+          style={styles.editButton}
+          onPress={() =>
+            navigation.navigate(...([NAVIGATION_PAGES.ADD_TRIP, { editTripId: item.id }] as never))
+          }
+        >
+          <EditIcon />
+          <Text style={styles.editButtonText}>Edit</Text>
+        </TouchableOpacity>
+      </View>
+
+      <View style={styles.divider} />
+
+      {item.description && (
+        <>
+          <Text style={styles.descriptionTitle}>Description</Text>
+          <Text style={styles.descriptionText}>{item.description}</Text>
+        </>
+      )}
+
+      <Text style={styles.visitedRegionsTitle}>Visited regions</Text>
+      <ScrollView
+        style={styles.regionsScrollView}
+        showsVerticalScrollIndicator={false}
+        horizontal={false}
+        nestedScrollEnabled={true}
+      >
+        {item.regions.map((region) => {
+          const name = region.region_name.split(/ – | - /);
+
+          return (
+            <View key={region.id} style={styles.regionItem}>
+              <Image source={{ uri: API_HOST + region.flag1 }} style={styles.flagIcon} />
+              {region.flag2 && (
+                <Image
+                  source={{ uri: API_HOST + region.flag2 }}
+                  style={[styles.flagIcon, { marginLeft: -5 }]}
+                />
+              )}
+              <View style={styles.nameContainer}>
+                <Text style={styles.regionName}>{name[0]}</Text>
+                <Text style={styles.regionSubname}>{name[1]}</Text>
+              </View>
+            </View>
+          );
+        })}
+      </ScrollView>
+    </View>
+  );
+};
+
+export default TripItem;

+ 100 - 0
src/screens/InAppScreens/TravelsScreen/Components/TripItem/styles.tsx

@@ -0,0 +1,100 @@
+import { StyleSheet } from 'react-native';
+import { Colors } from 'src/theme';
+
+export const styles = StyleSheet.create({
+  alignCenter: { alignItems: 'center', justifyContent: 'center', textAlign: 'center' },
+  tripItemContainer: {
+    paddingHorizontal: 16,
+    paddingTop: 16,
+    paddingBottom: 4,
+    marginVertical: 8,
+    backgroundColor: '#FAFAFA',
+    borderRadius: 8
+  },
+  tripHeaderContainer: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+    marginBottom: 16
+  },
+  tripDateContainer: {
+    flexDirection: 'row',
+    gap: 4,
+    alignItems: 'center'
+  },
+  tripDateText: {
+    fontSize: 14,
+    fontWeight: 'bold',
+    color: Colors.DARK_BLUE
+  },
+  editButton: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    borderRadius: 6,
+    paddingVertical: 8,
+    paddingHorizontal: 12,
+    borderWidth: 1,
+    borderColor: Colors.BORDER_LIGHT,
+    gap: 6
+  },
+  editButtonText: {
+    fontSize: 13,
+    color: Colors.DARK_BLUE,
+    fontWeight: 'bold'
+  },
+  divider: {
+    height: 1,
+    backgroundColor: Colors.DARK_LIGHT,
+    marginBottom: 16
+  },
+  descriptionTitle: {
+    fontSize: 12,
+    fontWeight: '600',
+    color: Colors.DARK_BLUE,
+    marginBottom: 8
+  },
+  descriptionText: {
+    fontSize: 12,
+    fontWeight: '500',
+    color: Colors.DARK_BLUE,
+    paddingHorizontal: 8,
+    marginBottom: 16
+  },
+  visitedRegionsTitle: {
+    fontSize: 12,
+    fontWeight: '600',
+    color: Colors.DARK_BLUE,
+    marginBottom: 8
+  },
+  regionsScrollView: {
+    maxHeight: 200,
+    paddingHorizontal: 8
+  },
+  regionItem: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    marginBottom: 12
+  },
+  flagIcon: {
+    width: 24,
+    height: 24,
+    resizeMode: 'cover',
+    borderWidth: 0.5,
+    borderRadius: 12,
+    borderColor: '#B4C2C7'
+  },
+  nameContainer: {
+    marginLeft: 12,
+    flexShrink: 1
+  },
+  regionName: {
+    fontSize: 14,
+    fontWeight: 'bold',
+    color: Colors.DARK_BLUE
+  },
+  regionSubname: {
+    fontSize: 12,
+    fontWeight: '500',
+    color: Colors.DARK_BLUE
+  }
+});

+ 109 - 13
src/screens/InAppScreens/TravelsScreen/TripsScreen/index.tsx

@@ -1,47 +1,143 @@
-import React, { useState } from 'react';
-import { View, Text, TouchableOpacity } from 'react-native';
-import { PageWrapper, Header, Modal, FlatList as List } from 'src/components';
+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 } from 'src/components';
+import TripItem from '../Components/TripItem';
+
+import { useGetTripsYearsQuery, useGetTripsForYearQuery } from '@api/trips';
+import { StoreType, storage } from 'src/storage';
+import { TripsData } from '../utils/types';
+import { NAVIGATION_PAGES } from 'src/types';
 import { styles } from './styles';
 
 import ChevronIcon from '../../../../../assets/icons/travels-screens/chevron-bottom.svg';
 import AddIcon from '../../../../../assets/icons/travels-screens/circle-plus.svg';
 import TripSvg from '../../../../../assets/icons/travels-screens/trip.svg';
 
-const TripsScreen = () => {
+const TripsScreen = ({ route }: { route: any }) => {
+  const token = storage.get('token', StoreType.STRING) as string;
+  const navigation = useNavigation();
   const [isDatePickerVisible, setDatePickerVisible] = useState(false);
+  const { data: years, refetch } = useGetTripsYearsQuery(token, true);
+  const [selectedYear, setSelectedYear] = useState<string>('');
+  const { data: tripsData, refetch: refetchTrips } = useGetTripsForYearQuery(
+    token,
+    selectedYear,
+    true
+  );
+  const [trips, setTrips] = useState<TripsData[]>([]);
+  const [isWarningModalVisible, setIsWarningModalVisible] = useState(false);
+
+  useFocusEffect(
+    useCallback(() => {
+      const fetchData = async () => {
+        try {
+          await refetch();
+          await refetchTrips();
+        } catch (error) {
+          console.error(error);
+        }
+      };
+
+      if (route.params?.saved || route.params?.updated || route.params?.deleted) {
+        fetchData();
+        setIsWarningModalVisible(true);
+      }
+    }, [route.params])
+  );
+
+  useEffect(() => {
+    if (years && years.data && selectedYear.length === 0) {
+      setSelectedYear(years.data[0]);
+    }
+  }, [years]);
+
+  useEffect(() => {
+    if (!isWarningModalVisible) {
+      navigation.setParams({ saved: false, updated: false, deleted: false } as never);
+    }
+  }, [isWarningModalVisible]);
+
+  useEffect(() => {
+    if (tripsData && tripsData.trips) {
+      setTrips(tripsData.trips);
+    }
+  }, [tripsData]);
+
+  const renderItem = useCallback(({ item }: { item: TripsData }) => <TripItem item={item} />, []);
+
+  const onAddNewTripPress = useCallback(() => {
+    navigation.navigate(NAVIGATION_PAGES.ADD_TRIP as never);
+  }, [navigation]);
+
   return (
     <PageWrapper>
       <Header label="Trips" />
       <View style={styles.tabContainer}>
         <TouchableOpacity style={styles.regionSelector} onPress={() => setDatePickerVisible(true)}>
-          <Text style={styles.regionText}>{'2024'}</Text>
+          <Text style={[styles.regionText]}>{selectedYear}</Text>
           <ChevronIcon />
         </TouchableOpacity>
-        <TouchableOpacity style={styles.addNewTab}>
+        <TouchableOpacity style={styles.addNewTab} onPress={onAddNewTripPress}>
           <AddIcon />
           <Text style={styles.addNewTabText}>Add New Trip</Text>
         </TouchableOpacity>
       </View>
-      <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', gap: 10 }}>
-        <View style={styles.noTripsIcon}>
-          <TripSvg fill={'rgb(242, 242, 242)'} />
+
+      {trips.length === 0 ? (
+        <View style={styles.noTripsContainer}>
+          <View style={styles.noTripsIcon}>
+            <TripSvg fill="#F2F2F2" />
+          </View>
+          <Text style={styles.noTripsText}>No trips at the moment</Text>
         </View>
-        <Text style={styles.noTripsText}>No trips at the moment</Text>
-      </View>
+      ) : (
+        <FlatList
+          data={trips}
+          renderItem={renderItem}
+          keyExtractor={(item) => item.id.toString()}
+          style={styles.tripsList}
+          contentContainerStyle={styles.tripsListContentContainer}
+          showsVerticalScrollIndicator={false}
+        />
+      )}
 
       <Modal
         onRequestClose={() => setDatePickerVisible(false)}
-        headerTitle={'Select Date'}
+        headerTitle={'Select Year'}
         visible={isDatePickerVisible}
       >
         <List
           itemObject={(object) => {
+            setSelectedYear(object);
             setDatePickerVisible(false);
           }}
-          initialData={['2024']}
+          initialData={years?.data}
           date={true}
         />
       </Modal>
+      <WarningModal
+        type={'success'}
+        isVisible={isWarningModalVisible}
+        onClose={() => {
+          setIsWarningModalVisible(false);
+        }}
+        title={
+          route.params?.saved
+            ? 'Trip added'
+            : route.params?.deleted
+              ? 'Trip deleted'
+              : 'Trip updated'
+        }
+        message={
+          route.params?.saved
+            ? 'Trip was successfully added to your list of trips'
+            : route.params?.deleted
+              ? 'This trip was successfully deleted.'
+              : 'Trip was successfully updated'
+        }
+      />
     </PageWrapper>
   );
 };

+ 9 - 2
src/screens/InAppScreens/TravelsScreen/TripsScreen/styles.tsx

@@ -2,7 +2,8 @@ import { StyleSheet } from 'react-native';
 import { Colors } from 'src/theme';
 
 export const styles = StyleSheet.create({
-  tabContainer: { flexDirection: 'row', gap: 10, alignItems: 'center' },
+  tabContainer: { flexDirection: 'row', gap: 16, alignItems: 'center', marginBottom: 8 },
+  noTripsContainer: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 10 },
   regionSelector: {
     flexDirection: 'row',
     alignItems: 'center',
@@ -38,5 +39,11 @@ export const styles = StyleSheet.create({
     alignItems: 'center',
     justifyContent: 'center'
   },
-  noTripsText: { color: Colors.DARK_BLUE, fontSize: 11, fontWeight: '600' }
+  noTripsText: { color: Colors.DARK_BLUE, fontSize: 11, fontWeight: '600' },
+  tripsList: {
+    flex: 1
+  },
+  tripsListContentContainer: {
+    paddingBottom: 16
+  }
 });

+ 11 - 0
src/screens/InAppScreens/TravelsScreen/utils/calculateRegion.ts

@@ -0,0 +1,11 @@
+import * as turf from '@turf/turf';
+
+export const calculateMapRegion = (bounds: turf.BBox) => {
+  const padding = 10;
+  return {
+    latitude: (bounds[1] + bounds[3]) / 2,
+    longitude: (bounds[0] + bounds[2]) / 2,
+    latitudeDelta: Math.abs(bounds[3] - bounds[1]) + padding,
+    longitudeDelta: Math.abs(bounds[2] - bounds[0]) + padding
+  };
+};

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

@@ -0,0 +1,8 @@
+export const qualityOptions = [
+  { id: 1, name: 'Transit' },
+  { id: 2, name: 'Minimal visit' },
+  { id: 3, name: 'Good visit' },
+  { id: 4, name: 'Worked here' },
+  { id: 5, name: 'Lived here' },
+  { id: 6, name: 'Travelguru' }
+];

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

@@ -49,3 +49,30 @@ export interface ImageStatus {
   dateTime: string | null;
   progress: number;
 }
+
+export interface TripsData {
+  id: number;
+  date_from: string;
+  date_to: string;
+  description: string;
+  regions: RegionTripsData[];
+}
+
+export interface RegionTripsData {
+  region_name: string;
+  flag1: string;
+  flag2: string | null;
+  status: 0 | 1;
+  id: number;
+}
+
+export interface RegionAddData {
+  id: number;
+  region_name: string;
+  quality?: number;
+  flag1?: string;
+  flag2?: string | null;
+  status?: 0 | 1;
+  hidden: boolean;
+  name?: string;
+}

+ 2 - 1
src/theme.ts

@@ -9,7 +9,8 @@ export enum Colors {
   DARK_LIGHT = '#E5E5E5',
   RED = '#EF5B5B',
   LIGHT_GRAY = '#C8C8C8',
-  TEXT_GRAY = '#3E6471'
+  TEXT_GRAY = '#3E6471',
+  BORDER_LIGHT = '#B7C6CB'
 }
 
 export function adaptiveStyle(

+ 18 - 3
src/types/api.ts

@@ -9,7 +9,8 @@ export enum API_ROUTE {
   AVATARS = 'avatars',
   STATISTICS = 'statistics',
   KYE = 'kye',
-  PHOTOS = 'photos'
+  PHOTOS = 'photos',
+  TRIPS = 'trips'
 }
 
 export enum API_ENDPOINT {
@@ -49,7 +50,14 @@ export enum API_ENDPOINT {
   REMOVE_TEMP = 'remove-temp',
   SAVE_TEMP = 'save-temp',
   DELETE_PHOTO = 'delete-photo',
-  UPDATE_PHOTO = 'update-photo'
+  UPDATE_PHOTO = 'update-photo',
+  GET_TRIPS_YEARS = 'get-trips-years',
+  GET_TRIPS_FOR_YEAR = 'get-trips-for-year-app',
+  GET_REGIONS_FOR_TRIPS = 'get-regions-for-trips',
+  GET_TRIP = 'get-trip',
+  SET_NEW_TRIP = 'new-trip',
+  UPDATE_TRIP = 'update-trip',
+  DELETE_TRIP = 'delete-trip'
 }
 
 export enum API {
@@ -88,7 +96,14 @@ export enum API {
   REMOVE_TEMP = `${API_ROUTE.PHOTOS}/${API_ENDPOINT.REMOVE_TEMP}`,
   SAVE_TEMP = `${API_ROUTE.PHOTOS}/${API_ENDPOINT.SAVE_TEMP}`,
   DELETE_PHOTO = `${API_ROUTE.PHOTOS}/${API_ENDPOINT.DELETE_PHOTO}`,
-  UPDATE_PHOTO = `${API_ROUTE.PHOTOS}/${API_ENDPOINT.UPDATE_PHOTO}`
+  UPDATE_PHOTO = `${API_ROUTE.PHOTOS}/${API_ENDPOINT.UPDATE_PHOTO}`,
+  GET_TRIPS_YEARS = `${API_ROUTE.TRIPS}/${API_ENDPOINT.GET_TRIPS_YEARS}`,
+  GET_TRIPS_FOR_YEAR = `${API_ROUTE.TRIPS}/${API_ENDPOINT.GET_TRIPS_FOR_YEAR}`,
+  GET_REGIONS_FOR_TRIPS = `${API_ROUTE.TRIPS}/${API_ENDPOINT.GET_REGIONS_FOR_TRIPS}`,
+  GET_TRIP = `${API_ROUTE.TRIPS}/${API_ENDPOINT.GET_TRIP}`,
+  SET_NEW_TRIP = `${API_ROUTE.TRIPS}/${API_ENDPOINT.SET_NEW_TRIP}`,
+  UPDATE_TRIP = `${API_ROUTE.TRIPS}/${API_ENDPOINT.UPDATE_TRIP}`,
+  DELETE_TRIP = `${API_ROUTE.TRIPS}/${API_ENDPOINT.DELETE_TRIP}`
 }
 
 export type BaseAxiosError = AxiosError;

+ 2 - 0
src/types/navigation.ts

@@ -29,4 +29,6 @@ export enum NAVIGATION_PAGES {
   MORE_PHOTOS = 'inAppMorePhotos',
   ADD_PHOTO = 'inAppAddPhoto',
   TRIPS = 'inAppTrips',
+  ADD_TRIP = 'inAppAddTrip',
+  ADD_REGIONS = 'inAppAddRegions',
 }