Просмотр исходного кода

Merge branch 'photos-screen' of SashaGoncharov19/nomadmania-app into dev

Viktoriia 1 год назад
Родитель
Сommit
7af84110ae
50 измененных файлов с 2190 добавлено и 87 удалено
  1. 6 0
      Route.tsx
  2. 1 0
      assets/icons/travels-screens/add-img.svg
  3. 10 0
      assets/icons/travels-screens/chevron-right.svg
  4. 10 0
      assets/icons/travels-screens/choose.svg
  5. 10 0
      assets/icons/travels-screens/circle-check.svg
  6. 10 0
      assets/icons/travels-screens/pen-to-square.svg
  7. 10 0
      assets/icons/travels-screens/save.svg
  8. 12 0
      assets/icons/travels-screens/three-dots.svg
  9. 3 0
      assets/icons/travels-screens/trash-solid.svg
  10. 12 0
      assets/icons/travels-screens/x-circle.svg
  11. 2 0
      package.json
  12. 2 1
      src/components/Calendars/RangeCalendar/Navigation/index.tsx
  13. 22 12
      src/components/Calendars/RangeCalendar/index.tsx
  14. 12 15
      src/components/Calendars/RangeCalendar/style.ts
  15. 38 22
      src/components/FlatList/index.tsx
  16. 38 16
      src/components/FlatList/item.tsx
  17. 13 1
      src/components/FlatList/styles.ts
  18. 13 5
      src/components/Modal/index.tsx
  19. 2 2
      src/components/Modal/style.ts
  20. 53 7
      src/components/WarningModal/index.tsx
  21. 3 0
      src/modules/api/photos/index.ts
  22. 112 0
      src/modules/api/photos/photos-api.tsx
  23. 9 0
      src/modules/api/photos/photos-query-keys.tsx
  24. 7 0
      src/modules/api/photos/queries/index.ts
  25. 22 0
      src/modules/api/photos/queries/use-post-delete-photo.tsx
  26. 17 0
      src/modules/api/photos/queries/use-post-get-photos-for-user.tsx
  27. 22 0
      src/modules/api/photos/queries/use-post-get-remove-temp.tsx
  28. 17 0
      src/modules/api/photos/queries/use-post-get-temp.tsx
  29. 21 0
      src/modules/api/photos/queries/use-post-set-save-temp.tsx
  30. 28 0
      src/modules/api/photos/queries/use-post-set-upload-temp.tsx
  31. 21 0
      src/modules/api/photos/queries/use-post-update-photo.tsx
  32. 316 0
      src/screens/InAppScreens/TravelsScreen/AddPhotoScreen/index.tsx
  33. 39 0
      src/screens/InAppScreens/TravelsScreen/AddPhotoScreen/styles.tsx
  34. 34 0
      src/screens/InAppScreens/TravelsScreen/Components/CustomButton.tsx
  35. 145 0
      src/screens/InAppScreens/TravelsScreen/Components/PhotoEditModal.tsx
  36. 103 0
      src/screens/InAppScreens/TravelsScreen/Components/PhotoItem.tsx
  37. 3 0
      src/screens/InAppScreens/TravelsScreen/Components/index.ts
  38. 120 1
      src/screens/InAppScreens/TravelsScreen/Components/styles.tsx
  39. 392 0
      src/screens/InAppScreens/TravelsScreen/MorePhotosScreen/index.tsx
  40. 66 0
      src/screens/InAppScreens/TravelsScreen/MorePhotosScreen/styles.tsx
  41. 248 0
      src/screens/InAppScreens/TravelsScreen/PhotosScreen/index.tsx
  42. 27 0
      src/screens/InAppScreens/TravelsScreen/PhotosScreen/styles.tsx
  43. 0 1
      src/screens/InAppScreens/TravelsScreen/Series/index.tsx
  44. 8 1
      src/screens/InAppScreens/TravelsScreen/index.tsx
  45. 19 0
      src/screens/InAppScreens/TravelsScreen/utils/groupPhotosByDate.tsx
  46. 2 0
      src/screens/InAppScreens/TravelsScreen/utils/index.ts
  47. 38 0
      src/screens/InAppScreens/TravelsScreen/utils/photoStyles.ts
  48. 51 0
      src/screens/InAppScreens/TravelsScreen/utils/types.ts
  49. 18 3
      src/types/api.ts
  50. 3 0
      src/types/navigation.ts

+ 6 - 0
Route.tsx

@@ -31,6 +31,9 @@ import StatisticsScreen from './src/screens/InAppScreens/TravellersScreen/Statis
 import SeriesScreen from 'src/screens/InAppScreens/TravelsScreen/Series';
 import { SeriesItemScreen } from 'src/screens/InAppScreens/TravelsScreen/SeriesItemScreen';
 import EarthScreen from 'src/screens/InAppScreens/TravelsScreen/EarthScreen';
+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 { NAVIGATION_PAGES } from './src/types';
 import { storage, StoreType } from './src/storage';
@@ -164,6 +167,9 @@ const Route = () => {
                     component={SeriesItemScreen}
                   />
                   <ScreenStack.Screen name={NAVIGATION_PAGES.EARTH} component={EarthScreen} />
+                  <ScreenStack.Screen name={NAVIGATION_PAGES.PHOTOS} component={PhotosScreen} />
+                  <ScreenStack.Screen name={NAVIGATION_PAGES.MORE_PHOTOS} component={MorePhotosScreen} />
+                  <ScreenStack.Screen name={NAVIGATION_PAGES.ADD_PHOTO} component={AddPhotoScreen} />
                 </ScreenStack.Navigator>
               )}
             </BottomTab.Screen>

Разница между файлами не показана из-за своего большого размера
+ 1 - 0
assets/icons/travels-screens/add-img.svg


+ 10 - 0
assets/icons/travels-screens/chevron-right.svg

@@ -0,0 +1,10 @@
+<svg width="10" height="11" viewBox="0 0 10 11" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_1693_17594)">
+<path d="M3.4154 1.5L6.70833 4.79293C7.09722 5.18182 7.09722 5.81818 6.70833 6.20707L3.4154 9.5" stroke="#0F3F4F" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+<defs>
+<clipPath id="clip0_1693_17594">
+<rect width="10" height="10" fill="white" transform="translate(0 0.501953)"/>
+</clipPath>
+</defs>
+</svg>

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

@@ -0,0 +1,10 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_1730_17157)">
+<path d="M8 1.5C9.72391 1.5 11.3772 2.18482 12.5962 3.40381C13.8152 4.62279 14.5 6.27609 14.5 8C14.5 9.72391 13.8152 11.3772 12.5962 12.5962C11.3772 13.8152 9.72391 14.5 8 14.5C6.27609 14.5 4.62279 13.8152 3.40381 12.5962C2.18482 11.3772 1.5 9.72391 1.5 8C1.5 6.27609 2.18482 4.62279 3.40381 3.40381C4.62279 2.18482 6.27609 1.5 8 1.5ZM8 16C10.1217 16 12.1566 15.1571 13.6569 13.6569C15.1571 12.1566 16 10.1217 16 8C16 5.87827 15.1571 3.84344 13.6569 2.34315C12.1566 0.842855 10.1217 0 8 0C5.87827 0 3.84344 0.842855 2.34315 2.34315C0.842855 3.84344 0 5.87827 0 8C0 10.1217 0.842855 12.1566 2.34315 13.6569C3.84344 15.1571 5.87827 16 8 16ZM11.5312 6.53125C11.825 6.2375 11.825 5.7625 11.5312 5.47188C11.2375 5.18125 10.7625 5.17813 10.4719 5.47188L7.00313 8.94063L5.53438 7.47188C5.24063 7.17813 4.76562 7.17813 4.475 7.47188C4.18437 7.76562 4.18125 8.24063 4.475 8.53125L6.475 10.5312C6.76875 10.825 7.24375 10.825 7.53438 10.5312L11.5312 6.53125Z" fill="#0F3F4F"/>
+</g>
+<defs>
+<clipPath id="clip0_1730_17157">
+<rect width="16" height="16" fill="white"/>
+</clipPath>
+</defs>
+</svg>

+ 10 - 0
assets/icons/travels-screens/circle-check.svg

@@ -0,0 +1,10 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_1740_17429)">
+<path d="M8 16C10.1217 16 12.1566 15.1571 13.6569 13.6569C15.1571 12.1566 16 10.1217 16 8C16 5.87827 15.1571 3.84344 13.6569 2.34315C12.1566 0.842855 10.1217 0 8 0C5.87827 0 3.84344 0.842855 2.34315 2.34315C0.842855 3.84344 0 5.87827 0 8C0 10.1217 0.842855 12.1566 2.34315 13.6569C3.84344 15.1571 5.87827 16 8 16ZM11.5312 6.53125L7.53125 10.5312C7.2375 10.825 6.7625 10.825 6.47188 10.5312L4.47188 8.53125C4.17813 8.2375 4.17813 7.7625 4.47188 7.47188C4.76562 7.18125 5.24062 7.17813 5.53125 7.47188L7 8.94063L10.4688 5.46875C10.7625 5.175 11.2375 5.175 11.5281 5.46875C11.8187 5.7625 11.8219 6.2375 11.5281 6.52812L11.5312 6.53125Z"/>
+</g>
+<defs>
+<clipPath id="clip0_1740_17429">
+<rect width="16" height="16" fill="white"/>
+</clipPath>
+</defs>
+</svg>

+ 10 - 0
assets/icons/travels-screens/pen-to-square.svg

@@ -0,0 +1,10 @@
+<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_1699_18642)">
+<path d="M16.5797 0.762988C15.8098 -0.0069336 14.5652 -0.0069336 13.7953 0.762988L12.7371 1.81768L16.1789 5.25947L17.2371 4.20127C18.007 3.43135 18.007 2.18682 17.2371 1.41689L16.5797 0.762988ZM6.06094 8.49736C5.84648 8.71182 5.68125 8.97549 5.58633 9.26729L4.5457 12.3892C4.44375 12.6915 4.52461 13.0255 4.74961 13.254C4.97461 13.4825 5.30859 13.5599 5.61445 13.4579L8.73633 12.4173C9.02461 12.3224 9.28828 12.1571 9.50625 11.9427L15.3879 6.05752L11.9426 2.61221L6.06094 8.49736ZM3.375 2.2501C1.51172 2.2501 0 3.76182 0 5.6251V14.6251C0 16.4884 1.51172 18.0001 3.375 18.0001H12.375C14.2383 18.0001 15.75 16.4884 15.75 14.6251V11.2501C15.75 10.6278 15.2473 10.1251 14.625 10.1251C14.0027 10.1251 13.5 10.6278 13.5 11.2501V14.6251C13.5 15.2474 12.9973 15.7501 12.375 15.7501H3.375C2.75273 15.7501 2.25 15.2474 2.25 14.6251V5.6251C2.25 5.00283 2.75273 4.5001 3.375 4.5001H6.75C7.37227 4.5001 7.875 3.99736 7.875 3.3751C7.875 2.75283 7.37227 2.2501 6.75 2.2501H3.375Z" fill="#0F3F4F"/>
+</g>
+<defs>
+<clipPath id="clip0_1699_18642">
+<rect width="18" height="18" fill="white"/>
+</clipPath>
+</defs>
+</svg>

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

@@ -0,0 +1,10 @@
+<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"/>
+</g>
+<defs>
+<clipPath id="clip0_1937_23992">
+<rect width="16" height="16" fill="white"/>
+</clipPath>
+</defs>
+</svg>

+ 12 - 0
assets/icons/travels-screens/three-dots.svg

@@ -0,0 +1,12 @@
+<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_1696_18021)">
+<path d="M11.5 5C11.5 5.82843 10.8284 6.5 10 6.5C9.17157 6.5 8.5 5.82843 8.5 5C8.5 4.17157 9.17157 3.5 10 3.5C10.8284 3.5 11.5 4.17157 11.5 5Z" fill="white"/>
+<path d="M11.5 10C11.5 10.8284 10.8284 11.5 10 11.5C9.17157 11.5 8.5 10.8284 8.5 10C8.5 9.17157 9.17157 8.5 10 8.5C10.8284 8.5 11.5 9.17157 11.5 10Z" fill="white"/>
+<path d="M10 16.5C10.8284 16.5 11.5 15.8284 11.5 15C11.5 14.1716 10.8284 13.5 10 13.5C9.17157 13.5 8.5 14.1716 8.5 15C8.5 15.8284 9.17157 16.5 10 16.5Z" fill="white"/>
+</g>
+<defs>
+<clipPath id="clip0_1696_18021">
+<rect width="20" height="20" fill="white"/>
+</clipPath>
+</defs>
+</svg>

+ 3 - 0
assets/icons/travels-screens/trash-solid.svg

@@ -0,0 +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"/>
+</svg>

+ 12 - 0
assets/icons/travels-screens/x-circle.svg

@@ -0,0 +1,12 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_1937_24043)">
+<rect y="0.00195312" width="24" height="24" rx="12" fill="white"/>
+<path d="M12 24.002C15.1826 24.002 18.2348 22.7377 20.4853 20.4872C22.7357 18.2368 24 15.1846 24 12.002C24 8.81936 22.7357 5.76711 20.4853 3.51667C18.2348 1.26624 15.1826 0.00195313 12 0.00195312C8.8174 0.00195313 5.76516 1.26624 3.51472 3.51667C1.26428 5.76711 0 8.81936 0 12.002C0 15.1846 1.26428 18.2368 3.51472 20.4872C5.76516 22.7377 8.8174 24.002 12 24.002ZM8.20312 8.20508C8.64375 7.76445 9.35625 7.76445 9.79219 8.20508L11.9953 10.4082L14.1984 8.20508C14.6391 7.76445 15.3516 7.76445 15.7875 8.20508C16.2234 8.6457 16.2281 9.3582 15.7875 9.79414L13.5844 11.9973L15.7875 14.2004C16.2281 14.641 16.2281 15.3535 15.7875 15.7895C15.3469 16.2254 14.6344 16.2301 14.1984 15.7895L11.9953 13.5863L9.79219 15.7895C9.35156 16.2301 8.63906 16.2301 8.20312 15.7895C7.76719 15.3488 7.7625 14.6363 8.20312 14.2004L10.4062 11.9973L8.20312 9.79414C7.7625 9.35352 7.7625 8.64102 8.20312 8.20508Z" fill="#0F3F4F"/>
+</g>
+<rect x="0.5" y="0.501953" width="23" height="23" rx="11.5" stroke="white"/>
+<defs>
+<clipPath id="clip0_1937_24043">
+<rect y="0.00195312" width="24" height="24" rx="12" fill="white"/>
+</clipPath>
+</defs>
+</svg>

+ 2 - 0
package.json

@@ -43,11 +43,13 @@
     "react-native-calendar-picker": "^7.1.4",
     "react-native-device-detection": "^0.2.1",
     "react-native-gesture-handler": "~2.12.0",
+    "react-native-image-viewing": "^0.2.2",
     "react-native-keyboard-aware-scroll-view": "^0.9.5",
     "react-native-maps": "1.7.1",
     "react-native-mmkv": "^2.11.0",
     "react-native-modal": "^13.0.1",
     "react-native-pager-view": "6.2.0",
+    "react-native-progress": "^5.0.1",
     "react-native-reanimated": "~3.3.0",
     "react-native-render-html": "^6.3.4",
     "react-native-safe-area-context": "4.6.3",

+ 2 - 1
src/components/Calendars/RangeCalendar/Navigation/index.tsx

@@ -4,11 +4,12 @@ import { View } from 'react-native';
 import { styles } from './style';
 import LeftArrow from '../../../../../assets/icons/left-arrow.svg';
 import RightArrow from '../../../../../assets/icons/right-arrow.svg';
+import { Colors } from 'src/theme';
 
 const Navigation = React.memo(({ direction }: { direction: 'prev' | 'next' }) => {
   return (
     <View style={[styles.navigationBtn, direction === 'prev' ? styles.prevComponent : styles.nextComponent]}>
-      {direction === 'prev' ? <LeftArrow /> : <RightArrow />}
+      {direction === 'prev' ? <LeftArrow fill={Colors.DARK_BLUE} /> : <RightArrow fill={Colors.DARK_BLUE} />}
     </View>
   );
 });

+ 22 - 12
src/components/Calendars/RangeCalendar/index.tsx

@@ -1,6 +1,9 @@
 import React, { useMemo, useState } from 'react';
 import { View } from 'react-native';
-import CalendarPicker, { CustomDatesStylesFunc, CustomDayHeaderStylesFunc } from 'react-native-calendar-picker';
+import CalendarPicker, {
+  CustomDatesStylesFunc,
+  CustomDayHeaderStylesFunc
+} from 'react-native-calendar-picker';
 import moment from 'moment';
 import { Modal } from '../../Modal';
 import Navigation from './Navigation';
@@ -8,9 +11,15 @@ import Navigation from './Navigation';
 import { styles } from './style';
 import { Colors } from '../../../theme';
 
-export default function RangeCalendar(
-  { isModalVisible, closeModal }: { isModalVisible: boolean, closeModal: () => void }
-) {
+export default function RangeCalendar({
+  isModalVisible,
+  closeModal,
+  allowRangeSelection = true
+}: {
+  isModalVisible: boolean;
+  closeModal: (selectedDate: Date | null) => void;
+  allowRangeSelection?: boolean;
+}) {
   const [selectedStartDate, setSelectedStartDate] = useState<Date | null>(null);
   const [selectedEndDate, setSelectedEndDate] = useState<Date | null>(null);
 
@@ -25,7 +34,7 @@ export default function RangeCalendar(
 
   const customDayHeaderStyles: CustomDayHeaderStylesFunc = () => {
     return {
-      textStyle: styles.dayHeader,
+      textStyle: styles.dayHeader
     };
   };
 
@@ -41,22 +50,22 @@ export default function RangeCalendar(
             height: 34,
             width: 34,
             textAlign: 'center',
-            verticalAlign: 'middle',
+            verticalAlign: 'middle'
           },
           style: {
-            backgroundColor: Colors.WHITE,
+            backgroundColor: Colors.WHITE
           }
         };
       }
       return {
         containerStyle: {},
-        textStyle: {},
+        textStyle: {}
       };
     };
   }, []);
 
   const resetSelections = () => {
-    closeModal();
+    closeModal(selectedStartDate);
     setSelectedStartDate(null);
     setSelectedEndDate(null);
   };
@@ -66,15 +75,15 @@ export default function RangeCalendar(
 
   return (
     <Modal
-      visibleInPercent={'70%'}
+      visibleInPercent={'auto'}
       visible={isModalVisible}
       onRequestClose={resetSelections}
-      headerTitle='Select Date'
+      headerTitle="Select Date"
     >
       <View style={styles.modalContent}>
         <CalendarPicker
           scaleFactor={400}
-          allowRangeSelection
+          allowRangeSelection={allowRangeSelection}
           allowBackwardRangeSelect
           onDateChange={handleOnDateChange as any}
           selectedStartDate={selectedStartDate as Date}
@@ -89,6 +98,7 @@ export default function RangeCalendar(
           selectedRangeStartTextStyle={styles.rangeStartEndTextStyle}
           selectedRangeEndStyle={[styles.rangeStartEndStyle, styles.rangeEndStyle]}
           selectedRangeStartStyle={[styles.rangeStartEndStyle, styles.rangeStartStyle]}
+          selectedDayTextColor={Colors.WHITE}
           customDayHeaderStyles={customDayHeaderStyles}
           customDatesStyles={customSelectedDatesStyles}
           disabledDatesTextStyle={styles.disabledDates}

+ 12 - 15
src/components/Calendars/RangeCalendar/style.ts

@@ -4,48 +4,46 @@ import { Colors } from '../../../theme';
 export const styles = StyleSheet.create({
   modalContent: {
     backgroundColor: Colors.WHITE,
-    padding: 22,
+    paddingTop: 22,
+    paddingBottom: 52,
     justifyContent: 'center',
-    alignItems: 'center',
+    alignItems: 'center'
   },
   rangeStartEndTextStyle: {
-    color: Colors.WHITE,
+    color: Colors.WHITE
   },
   rangeStartEndStyle: {
-    width: 40,
+    width: 40
   },
   rangeEndStyle: {
     alignSelf: 'flex-start',
     justifyContent: 'flex-start',
     borderTopRightRadius: 20,
-    borderBottomRightRadius: 20,
+    borderBottomRightRadius: 20
   },
   rangeStartStyle: {
     alignSelf: 'flex-end',
     justifyContent: 'flex-end',
     borderTopLeftRadius: 20,
-    borderBottomLeftRadius: 20,
+    borderBottomLeftRadius: 20
   },
   rangeStyle: {
     backgroundColor: Colors.DARK_LIGHT,
-    height: 34,
+    height: 34
   },
   labelsWrapper: {
     borderTopWidth: 0,
     borderBottomWidth: 0,
-    marginBottom: 5,
+    marginBottom: 5
   },
   disabledDates: {
-    width: "100%",
-    height: "100%",
     backgroundColor: Colors.WHITE,
-    textAlign: "center",
-    textAlignVertical: "center",
+    textAlign: 'center',
+    textAlignVertical: 'center'
   },
   dateTitle: {
     color: Colors.DARK_BLUE,
     fontSize: 16,
-    lineHeight: 24,
     fontWeight: '700'
   },
   headerWrapper: {
@@ -54,12 +52,11 @@ export const styles = StyleSheet.create({
   textStyle: {
     color: Colors.DARK_BLUE,
     fontSize: 14,
-    lineHeight: 20
+    lineHeight: 32
   },
   dayHeader: {
     color: Colors.ORANGE,
     fontSize: 12,
-    lineHeight: 16,
     letterSpacing: 1,
     fontWeight: '700'
   }

+ 38 - 22
src/components/FlatList/index.tsx

@@ -1,44 +1,56 @@
-import React, { FC, useEffect, useState } from 'react';
+import React, { FC, useCallback, useEffect, useState } from 'react';
 import { FlatList as List, SafeAreaView, View } from 'react-native';
 import { Input } from '../Input';
 import { styles } from './styles';
 import { Item, ItemData } from './item';
 import { useGetRegionsWithFlagQuery } from '@api/regions';
+import { useFocusEffect } from '@react-navigation/native';
+import { Loading } from '../Loading';
 
 type Props = {
   itemObject: (object: any) => void;
+  initialData?: ItemData[] | string[];
+  date?: boolean;
 };
 
 //TODO: rework to generic types + custom props
 
-export const FlatList: FC<Props> = ({ itemObject }) => {
-  const [selectedObject, setSelectedObject] = useState<{ name: string; id: number }>();
+export const FlatList: FC<Props> = ({ itemObject, initialData, date }) => {
+  const [selectedObject, setSelectedObject] = useState<{ name: string; id: number } | string>();
   const [search, setSearch] = useState('');
-  const [filteredData, setFilteredData] = useState<ItemData[]>([]);
-  const [masterData, setMasterData] = useState<ItemData[]>([]);
+  const [filteredData, setFilteredData] = useState<ItemData[] | string[]>([]);
+  const [masterData, setMasterData] = useState<ItemData[] | string[]>([]);
+  const [loading, setLoading] = useState<boolean>(true);
 
   const { data } = useGetRegionsWithFlagQuery(true);
 
-  useEffect(() => {
-    if (data) {
-      setFilteredData(data.data);
-      setMasterData(data.data);
-    }
-  }, [data]);
+  useFocusEffect(
+    useCallback(() => {
+      const dataToUse = initialData || data?.data;
+      if (dataToUse) {
+        setFilteredData(dataToUse);
+        setMasterData(dataToUse);
+      }
+      setLoading(false);
+    }, [data, initialData])
+  );
 
-  const selectItem = (object: { name: string; id: number }) => {
+  if (loading) return <Loading />;
+
+  const selectItem = (object: { name: string; id: number } | string) => {
     itemObject(object);
     setSelectedObject(object);
   };
 
   const searchFilter = (text: string) => {
     if (text) {
-      const newData = masterData.filter((item) => {
+      const newData = masterData.filter((item: any) => {
         const itemData = item.name ? item.name.toLowerCase() : ''.toLowerCase();
+        const initialData = item.country ? item.country.toLowerCase() : ''.toLowerCase();
         const textData = text.toLowerCase();
-        return itemData.indexOf(textData) > -1;
+        return itemData.indexOf(textData) > -1 || initialData.indexOf(textData) > -1;
       });
-      setFilteredData(newData);
+      setFilteredData(newData as string[] | ItemData[]);
       setSearch(text);
     } else {
       setFilteredData(masterData);
@@ -47,35 +59,39 @@ export const FlatList: FC<Props> = ({ itemObject }) => {
   };
 
   const renderItem = ({ item }: { item: ItemData }) => {
-    const selected = item.id === selectedObject?.id;
-
+    const selected = date
+      ? item === selectedObject
+      : item?.id === (selectedObject as { name: string; id: number })?.id;
     const backgroundColor = selected ? '#FAFAFA' : 'white';
 
     return (
       <Item
         selected={selected}
         item={item}
-        onPress={() => selectItem(item)}
+        onPress={() => selectItem(item as string | { name: string; id: number })}
         backgroundColor={backgroundColor}
+        initial={initialData ? true : false}
+        date={date}
       />
     );
   };
 
   return (
     <SafeAreaView style={styles.container}>
-      <View style={{ marginTop: 10 }}>
+      {!date && (
         <Input
           inputMode={'search'}
           placeholder={'Search'}
           onChange={(text) => searchFilter(text)}
           value={search}
         />
-      </View>
+      )}
       <List
-        data={filteredData}
+        data={filteredData as ItemData[]}
         renderItem={renderItem}
-        keyExtractor={(item) => item.id.toString()}
+        keyExtractor={(item) => (date ? item.toString() : item.id.toString())}
         extraData={selectedObject}
+        showsVerticalScrollIndicator={false}
       />
     </SafeAreaView>
   );

+ 38 - 16
src/components/FlatList/item.tsx

@@ -6,34 +6,54 @@ import { styles } from './styles';
 import MarkSVG from '../../../assets/icons/mark.svg';
 import { API_HOST } from '../../constants';
 
-export const Item = ({ item, onPress, backgroundColor, selected }: ItemProps) => {
-  const name = item.name.split('–');
+export const Item = ({ item, onPress, backgroundColor, selected, initial, date }: ItemProps) => {
+  const name = initial && date ? item : initial ? item.country : item.name?.split('–') || '';
 
   return (
     <TouchableOpacity onPress={onPress} style={[styles.item, { backgroundColor }]}>
+      {item?.country === 'All Regions' && <View style={{ width: 20 }}></View>}
       <View style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: 10 }}>
-        <Image
-          width={48}
-          height={48}
-          style={{ borderRadius: 48 / 2 }}
-          source={{
-            uri: `${API_HOST}/img/flags_new/${item.flag}`
-          }}
-        />
-        <View>
-          <Text style={[styles.title, { color: Colors.DARK_BLUE }]}>{name[0]}</Text>
-          <Text style={[styles.text, { color: Colors.DARK_BLUE }]}>{name[1]}</Text>
+        {item.flag && (
+          <Image
+            width={48}
+            height={48}
+            style={{ borderRadius: 48 / 2, borderWidth: 1, borderColor: Colors.LIGHT_GRAY }}
+            source={{
+              uri: initial ? `${API_HOST}${item.flag}` : `${API_HOST}/img/flags_new/${item.flag}`
+            }}
+          />
+        )}
+        <View style={{ flexShrink: 1 }}>
+          <View style={{ flexDirection: 'row' }}>
+            <Text style={[styles.title, { color: Colors.DARK_BLUE, flexShrink: 1 }]}>
+              {initial ? (name as string) : (name as string[])[0]}
+            </Text>
+            {initial && !date && item.country !== 'All Regions' && (
+              <View style={styles.regionIndicator}>
+                <Text style={[styles.text, { color: Colors.WHITE, fontWeight: 'bold' }]}>
+                  {item.dare ? 'DARE' : 'NM'}
+                </Text>
+              </View>
+            )}
+          </View>
+          {item.name && (
+            <Text style={[styles.text, { color: Colors.DARK_BLUE }]}>
+              {initial ? item.name : (name as string[])[1]}
+            </Text>
+          )}
         </View>
       </View>
-      <View style={{ marginRight: 10 }}>{selected && <MarkSVG />}</View>
+      <View style={{ marginRight: 10, width: 20 }}>{selected && <MarkSVG />}</View>
     </TouchableOpacity>
   );
 };
 
 export type ItemData = {
   id: number;
-  name: string;
-  flag: string;
+  name?: string;
+  flag?: string;
+  country?: string;
+  dare?: boolean;
 };
 
 type ItemProps = {
@@ -41,4 +61,6 @@ type ItemProps = {
   onPress: () => void;
   backgroundColor: string;
   selected: boolean;
+  initial?: boolean;
+  date?: boolean;
 };

+ 13 - 1
src/components/FlatList/styles.ts

@@ -1,10 +1,12 @@
 import { StatusBar, StyleSheet } from 'react-native';
 import { getFontSize } from '../../utils';
+import { Colors } from 'src/theme';
 
 export const styles = StyleSheet.create({
   container: {
     flex: 1,
-    gap: 15
+    gap: 15,
+    paddingVertical: 10
   },
   item: {
     width: '100%',
@@ -20,5 +22,15 @@ export const styles = StyleSheet.create({
   },
   text: {
     fontSize: getFontSize(12)
+  },
+  regionIndicator: {
+    height: 18,
+    overflow: 'hidden',
+    backgroundColor: Colors.DARK_BLUE,
+    borderRadius: 9,
+    paddingHorizontal: 6,
+    alignItems: 'center',
+    justifyContent: 'center',
+    marginLeft: 4
   }
 });

+ 13 - 5
src/components/Modal/index.tsx

@@ -1,5 +1,11 @@
 import React, { FC, ReactNode } from 'react';
-import { Dimensions, DimensionValue, PixelRatio, View } from 'react-native';
+import {
+  Dimensions,
+  DimensionValue,
+  Platform,
+  StatusBar,
+  View
+} from 'react-native';
 import ReactModal from 'react-native-modal';
 import { ModalHeader } from './ModalHeader/modal-header';
 import { styles } from './style';
@@ -12,8 +18,6 @@ type Props = {
   headerTitle: string;
 };
 
-// TODO: heightPercentageToDP for all devices
-
 export const Modal: FC<Props> = ({
   children,
   onRequestClose,
@@ -21,7 +25,11 @@ export const Modal: FC<Props> = ({
   visibleInPercent,
   headerTitle
 }) => {
-  const screenHeight = Dimensions.get('window').height;
+  const screenHeight = Dimensions.get('screen').height;
+  const NOTCH_HEIGHT = 44;
+  const statusBarHeight =
+    Platform.OS === 'ios' ? StatusBar.currentHeight || NOTCH_HEIGHT : StatusBar.currentHeight || 0;
+  const adjustedHeight = screenHeight - statusBarHeight;
 
   return (
     <ReactModal
@@ -32,7 +40,7 @@ export const Modal: FC<Props> = ({
       statusBarTranslucent={true}
       presentationStyle="overFullScreen"
     >
-      <View style={[styles.wrapper, { height: visibleInPercent ?? screenHeight }]}>
+      <View style={[styles.wrapper, { height: visibleInPercent ?? adjustedHeight }]}>
         <ModalHeader onRequestClose={onRequestClose} textHeader={headerTitle} />
         <Drawer />
         {children}

+ 2 - 2
src/components/Modal/style.ts

@@ -16,6 +16,6 @@ export const styles = StyleSheet.create({
   },
   modal: {
     justifyContent: 'flex-end',
-    margin: 0,
-  },
+    margin: 0
+  }
 });

+ 53 - 7
src/components/WarningModal/index.tsx

@@ -14,18 +14,32 @@ import { Button } from '../Button';
 export const WarningModal = ({
   isVisible,
   onClose,
-  type
+  type,
+  message,
+  title,
+  action
 }: {
   isVisible: boolean;
   onClose: () => void;
   type: string;
+  message?: string;
+  title?: string;
+  action?: () => void;
 }) => {
   const navigation = useNavigation();
 
   const content = {
     offline: {
       message: 'Please check your Internet connection and try again.',
-      buttons: [{ text: 'OK', textColor: Colors.WHITE, color: Colors.DARK_BLUE, action: onClose }]
+      buttons: [
+        {
+          text: 'OK',
+          textColor: Colors.WHITE,
+          color: Colors.DARK_BLUE,
+          action: onClose,
+          borderColor: Colors.DARK_BLUE
+        }
+      ]
     },
     unauthorized: {
       message: 'To use this feature you need to have an account with NomadMania.',
@@ -37,7 +51,8 @@ export const WarningModal = ({
           action: () => {
             onClose();
             navigation.navigate(NAVIGATION_PAGES.LOGIN as never);
-          }
+          },
+          borderColor: Colors.DARK_BLUE
         },
         {
           text: 'Register',
@@ -46,7 +61,32 @@ export const WarningModal = ({
           action: () => {
             onClose();
             navigation.navigate(NAVIGATION_PAGES.REGISTER as never);
-          }
+          },
+          borderColor: Colors.DARK_BLUE
+        }
+      ]
+    },
+    delete: {
+      message,
+      buttons: [
+        {
+          text: 'No',
+          textColor: Colors.DARK_BLUE,
+          color: Colors.WHITE,
+          action: () => {
+            onClose();
+          },
+          borderColor: Colors.DARK_BLUE
+        },
+        {
+          text: 'Delete',
+          textColor: Colors.WHITE,
+          color: Colors.RED,
+          action: () => {
+            onClose();
+            action && action();
+          },
+          borderColor: Colors.RED
         }
       ]
     }
@@ -64,19 +104,25 @@ export const WarningModal = ({
             </TouchableOpacity>
           </View>
           <View style={styles.modalContent}>
-            <Text style={styles.modalTitle}>Oops!</Text>
+            <Text style={styles.modalTitle}>{title ?? 'Oops!'}</Text>
             <Text style={styles.modalText}>{modalContent.message}</Text>
             <View style={styles.buttonContainer}>
               {modalContent.buttons.map(
                 (
-                  button: { text: string; textColor: string; color: string; action: () => void },
+                  button: {
+                    text: string;
+                    textColor: string;
+                    color: string;
+                    action: () => void;
+                    borderColor: string;
+                  },
                   idx: number
                 ) => (
                   <Button
                     key={idx}
                     variant={ButtonVariants.OPACITY}
                     containerStyles={{
-                      borderColor: Colors.DARK_BLUE,
+                      borderColor: button.borderColor,
                       backgroundColor: button.color,
                       width: type === 'offline' ? '60%' : '45%'
                     }}

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

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

+ 112 - 0
src/modules/api/photos/photos-api.tsx

@@ -0,0 +1,112 @@
+import { request } from '../../../utils';
+import { API } from '../../../types';
+import { ResponseType } from '../response-type';
+
+export interface PostGetPhotosReturn extends ResponseType {
+  all_regions: {
+    id: number;
+    dare: boolean;
+    nm: boolean;
+    country: string;
+    name: string;
+    flag: string;
+  }[];
+  by_date: {
+    [key: string]: PhotosData[];
+  };
+  by_region: {
+    id: number;
+    country: string;
+    name: string | null;
+    flag: string;
+    nm: boolean;
+    dare: boolean;
+    photos: PhotosData[];
+  }[];
+  by_album: {
+    album_id: number;
+    album_name: string;
+    album_cover_photo: string;
+    photos: PhotosData[];
+  }[];
+}
+
+interface PhotosData {
+  id: number;
+  region_nm: number;
+  region_dare: number;
+  region: number;
+  title: string;
+  date: string;
+  url_small: string;
+  url_mid: string;
+  url_full: string;
+  album_id: number;
+  album_name: string;
+}
+
+export interface PostSetTempData {
+  token: string;
+  file: {
+    type: string;
+    uri: string;
+    name: string;
+  };
+}
+
+export interface PostSetSaveTemp {
+  token: string;
+  guids: string[];
+  date: string | null;
+  region: number;
+  description: string | null;
+}
+
+export interface PostGetTempReturn extends ResponseType {
+  photos: {
+    guid: string;
+    link: string;
+  }[];
+}
+
+export interface PostSetTempReturn extends ResponseType {
+  guid: string;
+  link: string;
+}
+
+export interface PostSetSaveTempReturn extends ResponseType {
+  saved: string[];
+  errors: string[];
+}
+
+export interface PostSetUpdatePhoto {
+  token: string;
+  photo_id: number;
+  date: string | null;
+  region: number | null;
+  description: string | null;
+}
+
+export const photosApi = {
+  getPhotosForUser: (token: string) =>
+    request.postForm<PostGetPhotosReturn>(API.GET_PHOTOS_FOR_USER, { token }),
+  setUploadTemp: (data: PostSetTempData) => {
+    const formData = new FormData();
+
+    formData.append('token', JSON.stringify(data.token));
+    formData.append('file', {
+      ...data.file
+    } as unknown as Blob);
+
+    return request.postForm<PostSetTempReturn>(API.UPLOAD_TEMP, formData);
+  },
+  getTemp: (token: string) => request.postForm<PostGetTempReturn>(API.GET_TEMP, { token }),
+  removeTemp: (token: string, guid: string) =>
+    request.postForm<ResponseType>(API.REMOVE_TEMP, { token, guid }),
+  saveTemp: (data: PostSetSaveTemp) => request.postForm<PostSetSaveTempReturn>(API.SAVE_TEMP, data),
+  deletePhoto: (token: string, photo_id: number) =>
+    request.postForm<ResponseType>(API.DELETE_PHOTO, { token, photo_id }),
+  updatePhoto: (data: PostSetUpdatePhoto) =>
+    request.postForm<PostSetSaveTempReturn>(API.UPDATE_PHOTO, data)
+};
+export { ResponseType };

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

@@ -0,0 +1,9 @@
+export const photosQueryKeys = {
+  getPhotosForUser: (token: string) => ['getPhotosForUser', { token }] as const,
+  setUploadTemp: () => ['setUploadTemp'] as const,
+  getTemp: (token: string) => ['getTemp', { token }],
+  removeTemp: () => ['removeTemp'] as const,
+  saveTemp: () => ['saveTemp'] as const,
+  deletePhoto: () => ['deletePhoto'] as const,
+  updatePhoto: () => ['updatePhoto'] as const
+};

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

@@ -0,0 +1,7 @@
+export * from './use-post-get-photos-for-user';
+export * from './use-post-set-upload-temp';
+export * from './use-post-get-temp';
+export * from './use-post-get-remove-temp';
+export * from './use-post-set-save-temp';
+export * from './use-post-delete-photo';
+export * from './use-post-update-photo';

+ 22 - 0
src/modules/api/photos/queries/use-post-delete-photo.tsx

@@ -0,0 +1,22 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { photosQueryKeys } from '../photos-query-keys';
+import { photosApi } from '../photos-api';
+import { ResponseType } from '../../response-type';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const usePostDeletePhotoMutation = () => {
+  return useMutation<
+    ResponseType,
+    BaseAxiosError,
+    { token: string; photo_id: number },
+    ResponseType
+  >({
+    mutationKey: photosQueryKeys.deletePhoto(),
+    mutationFn: async (variables) => {
+      const response = await photosApi.deletePhoto(variables.token, variables.photo_id);
+      return response.data;
+    }
+  });
+};

+ 17 - 0
src/modules/api/photos/queries/use-post-get-photos-for-user.tsx

@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { photosQueryKeys } from '../photos-query-keys';
+import { photosApi, type PostGetPhotosReturn } from '../photos-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const useGetPhotosForUserQuery = (token: string, enabled: boolean) => {
+  return useQuery<PostGetPhotosReturn, BaseAxiosError>({
+    queryKey: photosQueryKeys.getPhotosForUser(token),
+    queryFn: async () => {
+      const response = await photosApi.getPhotosForUser(token);
+      return response.data;
+    },
+    enabled
+  });
+};

+ 22 - 0
src/modules/api/photos/queries/use-post-get-remove-temp.tsx

@@ -0,0 +1,22 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { photosQueryKeys } from '../photos-query-keys';
+import { photosApi } from '../photos-api';
+import { ResponseType } from '../../response-type';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const usePostRemoveTempMutation = () => {
+  return useMutation<
+    ResponseType,
+    BaseAxiosError,
+    { token: string; guid: string },
+    ResponseType
+  >({
+    mutationKey: photosQueryKeys.removeTemp(),
+    mutationFn: async (variables) => {
+      const response = await photosApi.removeTemp(variables.token, variables.guid);
+      return response.data;
+    }
+  });
+};

+ 17 - 0
src/modules/api/photos/queries/use-post-get-temp.tsx

@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { photosQueryKeys } from '../photos-query-keys';
+import { photosApi, type PostGetTempReturn } from '../photos-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const useGetTempQuery = (token: string, enabled: boolean) => {
+  return useQuery<PostGetTempReturn, BaseAxiosError>({
+    queryKey: photosQueryKeys.getTemp(token),
+    queryFn: async () => {
+      const response = await photosApi.getTemp(token);
+      return response.data;
+    },
+    enabled,
+  });
+};

+ 21 - 0
src/modules/api/photos/queries/use-post-set-save-temp.tsx

@@ -0,0 +1,21 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { photosQueryKeys } from '../photos-query-keys';
+import { type PostSetSaveTempReturn, type PostSetSaveTemp, photosApi } from '../photos-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const usePostSaveTempMutation = () => {
+  return useMutation<
+    PostSetSaveTempReturn,
+    BaseAxiosError,
+    PostSetSaveTemp,
+    PostSetSaveTempReturn
+  >({
+    mutationKey: photosQueryKeys.saveTemp(),
+    mutationFn: async (data) => {
+      const response = await photosApi.saveTemp(data);
+      return response.data;
+    }
+  });
+};

+ 28 - 0
src/modules/api/photos/queries/use-post-set-upload-temp.tsx

@@ -0,0 +1,28 @@
+import axios from 'axios';
+import { type PostSetTempData } from '../photos-api';
+
+import { API } from '../../../../types';
+import { API_URL } from 'src/constants';
+
+export const postSetUploadTemp = async (data: PostSetTempData) => {
+  const url = API_URL + '/' + API.UPLOAD_TEMP;
+  const formData = new FormData();
+  formData.append('token', data.token);
+  formData.append('file', data.file as unknown as Blob);
+
+  const config = {
+    onUploadProgress: (progressEvent: ProgressEvent) => {
+      if (progressEvent.lengthComputable) {
+        const progress = progressEvent.loaded / progressEvent.total;
+        console.log('Upload progress:', progress);
+      }
+    }
+  };
+
+  try {
+    const response = await axios.post(url, formData, config as any);
+    return response.data;
+  } catch (error) {
+    console.error('Error', error);
+  }
+};

+ 21 - 0
src/modules/api/photos/queries/use-post-update-photo.tsx

@@ -0,0 +1,21 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { photosQueryKeys } from '../photos-query-keys';
+import { type PostSetSaveTempReturn, type PostSetUpdatePhoto, photosApi } from '../photos-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const usePostUpdatePhotoMutation = () => {
+  return useMutation<
+    PostSetSaveTempReturn,
+    BaseAxiosError,
+    PostSetUpdatePhoto,
+    PostSetSaveTempReturn
+  >({
+    mutationKey: photosQueryKeys.updatePhoto(),
+    mutationFn: async (data) => {
+      const response = await photosApi.updatePhoto(data);
+      return response.data;
+    }
+  });
+};

+ 316 - 0
src/screens/InAppScreens/TravelsScreen/AddPhotoScreen/index.tsx

@@ -0,0 +1,316 @@
+import React, { useEffect, useState } from 'react';
+import { View, FlatList, Image, Text, TouchableOpacity } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+import * as Progress from 'react-native-progress';
+import * as ImagePicker from 'expo-image-picker';
+
+import { Header, Input, Modal, PageWrapper, FlatList as List } from 'src/components';
+import { API_HOST } from 'src/constants';
+import { CustomButton } from '../Components';
+import RangeCalendar from 'src/components/Calendars/RangeCalendar';
+import { postSetUploadTemp, usePostRemoveTempMutation, usePostSaveTempMutation } from '@api/photos';
+
+import { StoreType, storage } from 'src/storage';
+import { AllRegions, ImageStatus, TempData } from '../utils/types';
+import { itemWidth } from '../utils';
+import { styles } from './styles';
+import { Colors } from 'src/theme';
+
+import AddImgSvg from '../../../../../assets/icons/travels-screens/add-img.svg';
+import ChooseSvg from '../../../../../assets/icons/travels-screens/choose.svg';
+import SaveSvg from '../../../../../assets/icons/travels-screens/save.svg';
+import XCircleSvg from '../../../../../assets/icons/travels-screens/x-circle.svg';
+import ChevronIcon from '../../../../../assets/icons/chevron-left.svg';
+import CalendarSvg from '../../../../../assets/icons/calendar.svg';
+
+const AddPhotoScreen = ({ route }: { route: any }) => {
+  const navigation: any = useNavigation();
+  const { data, images, allRegions, tempData } = route.params;
+  const token = storage.get('token', StoreType.STRING) as string;
+  const [isModalVisible, setIsModalVisible] = useState(false);
+  const [selectedRegion, setSelectedRegion] = useState<AllRegions | null>(
+    data && data.country
+      ? ({
+          id: data.id,
+          country: data.country
+        } as AllRegions)
+      : null
+  );
+  const [description, setDescription] = useState('');
+  const [selectedDate, setSelectedDate] = useState<string | null>(
+    data && data.date ? data.date : null
+  );
+  const { mutate: removeTemp } = usePostRemoveTempMutation();
+  const { mutate: saveTemp, data: saveResponse } = usePostSaveTempMutation();
+  const [imagesStatus, setImagesStatus] = useState<ImageStatus[]>(
+    images?.map((image: ImagePicker.ImagePickerAsset) => ({
+      uri: image.uri,
+      assetId: image.assetId,
+      loaded: false,
+      progress: 0,
+      uploadResponse: null,
+      dateTime: image.exif?.DateTimeOriginal?.split(' ')[0].replaceAll(':', '-') ?? null
+    }))
+  );
+  const [calendarVisible, setCalendarVisible] = useState(false);
+
+  useEffect(() => {
+    if (tempData) {
+      setImagesStatus(
+        tempData.map((temp: TempData) => {
+          return { assetId: temp.guid, loaded: true, uploadResponse: temp };
+        })
+      );
+      return;
+    } else {
+      images.forEach((image: ImagePicker.ImagePickerAsset) => uploadTempImage(image));
+    }
+
+    const date = images[images.length - 1]?.exif?.DateTimeOriginal?.split(' ')[0].replaceAll(
+      ':',
+      '-'
+    );
+    date && setSelectedDate(date);
+  }, [images]);
+
+  const uploadTempImage = async (image: ImagePicker.ImagePickerAsset) => {
+    const uriParts = image.uri.split('.');
+    const fileType = uriParts[uriParts.length - 1];
+
+    const imgData = {
+      token,
+      file: {
+        uri: image.uri,
+        name: image.uri.split('/').pop()!,
+        type: `image/${fileType}`
+      }
+    };
+
+    image.assetId && simulateUploadProgress(image.assetId);
+
+    await postSetUploadTemp(imgData)
+      .then((response) => {
+        if (response && response.result === 'OK') {
+          setImagesStatus((currentStatus) =>
+            currentStatus.map((item) => {
+              if (item.assetId === image.assetId) {
+                return {
+                  ...item,
+                  loaded: true,
+                  progress: 1,
+                  uploadResponse: { guid: response.guid, link: response.link }
+                };
+              }
+              return item;
+            })
+          );
+        }
+      })
+      .catch((error) => {
+        console.error('Error', error);
+      });
+  };
+
+  const simulateUploadProgress = (assetId: string) => {
+    let progress = 0;
+    const intervalId = setInterval(() => {
+      progress += 0.05 + Math.random() * 0.25;
+      if (progress >= 1) {
+        clearInterval(intervalId);
+        progress = 1;
+      }
+
+      setImagesStatus((currentStatus) =>
+        currentStatus.map((item) => {
+          if (item.assetId === assetId) {
+            return { ...item, progress };
+          }
+          return item;
+        })
+      );
+    }, 300);
+  };
+
+  const deleteTemp = (item: ImageStatus) => {
+    removeTemp(
+      { token, guid: item.uploadResponse?.guid as string },
+      {
+        onSuccess: (res) => {
+          if (res.result === 'OK') {
+            setImagesStatus(
+              imagesStatus.filter(
+                (img: ImageStatus) => img.uploadResponse?.guid !== item.uploadResponse?.guid
+              )
+            );
+          }
+        }
+      }
+    );
+  };
+
+  const handleSelectPhoto = async () => {
+    await ImagePicker.launchImageLibraryAsync({
+      mediaTypes: ImagePicker.MediaTypeOptions.Images,
+      quality: 1,
+      allowsMultipleSelection: true,
+      exif: true
+    }).then((result) => {
+      if (!result.canceled) {
+        addPhoto(result.assets);
+      }
+    });
+  };
+
+  const addPhoto = (newImages: ImagePicker.ImagePickerAsset[]) => {
+    setSelectedDate(
+      newImages[newImages.length - 1]?.exif?.DateTimeOriginal?.split(' ')[0].replaceAll(':', '-')
+    );
+    setImagesStatus([
+      ...imagesStatus,
+      ...newImages.map((image: any) => ({
+        uri: image.uri,
+        assetId: image.assetId,
+        loaded: false,
+        progress: 0,
+        uploadResponse: null,
+        dateTime: image.exif?.DateTimeOriginal?.split(' ')[0].replaceAll(':', '-') ?? null
+      }))
+    ]);
+    newImages.forEach((image: ImagePicker.ImagePickerAsset) => uploadTempImage(image));
+  };
+
+  const saveTempData = () => {
+    const tempData = {
+      token,
+      guids: imagesStatus.map((img) => img.uploadResponse?.guid!),
+      date: selectedDate,
+      region: selectedRegion?.id!,
+      description
+    };
+    saveTemp(tempData, {
+      onSuccess: () => {
+        data ? navigation.pop(2) : navigation.goBack();
+      }
+    });
+  };
+
+  const renderItem = ({ item }: { item: ImageStatus }) => (
+    <View style={styles.itemContainer}>
+      <View style={{ position: 'relative' }}>
+        {!item.loaded ? (
+          <Progress.Bar
+            progress={item.progress}
+            width={itemWidth}
+            color={Colors.DARK_BLUE}
+            borderWidth={0}
+            borderRadius={5}
+            unfilledColor="rgba(0, 0, 0, 0.1)"
+          />
+        ) : (
+          <Image
+            source={{ uri: API_HOST + item.uploadResponse?.link }}
+            style={[styles.image, { width: itemWidth, height: itemWidth }]}
+          />
+        )}
+        <TouchableOpacity
+          style={[styles.deleteTemp, !item.loaded && { top: -18, right: -10 }]}
+          onPress={() => deleteTemp(item)}
+        >
+          <XCircleSvg />
+        </TouchableOpacity>
+      </View>
+    </View>
+  );
+
+  const getHeaderLabel = () => {
+    if (data?.country) return data.country;
+    if (data?.date) return data.date;
+    return 'Add photo';
+  };
+
+  return (
+    <PageWrapper>
+      <Header label={getHeaderLabel()} />
+      <View style={{ alignItems: 'center', marginBottom: 8 }}>
+        {data?.name && <Text style={styles.title}>{data.name}</Text>}
+        <View style={styles.btnContainer}>
+          <CustomButton
+            title="Save"
+            onPress={saveTempData}
+            icon={<SaveSvg fill={Colors.DARK_BLUE} />}
+            disabled={!selectedRegion || imagesStatus.length === 0}
+          />
+
+          <CustomButton
+            title="Choose"
+            icon={<ChooseSvg fill={Colors.DARK_BLUE} />}
+            onPress={() => {}}
+            disabled={true}
+          />
+          <CustomButton
+            title="Add photo"
+            icon={<AddImgSvg fill={Colors.DARK_BLUE} />}
+            onPress={handleSelectPhoto}
+          />
+        </View>
+      </View>
+      <View style={{ gap: 10, marginVertical: 8 }}>
+        <Input
+          placeholder="Photo description"
+          inputMode={'text'}
+          onChange={(text) => setDescription(text)}
+          value={description}
+        />
+        {!data?.country && (
+          <TouchableOpacity
+            style={[styles.regionSelector, { justifyContent: 'space-between' }]}
+            onPress={() => setIsModalVisible(true)}
+          >
+            <Text style={styles.regionText}>{selectedRegion?.country ?? 'Choose a region'}</Text>
+            <ChevronIcon fill={'#C8C8C8'} style={{ transform: [{ rotate: '-90deg' }] }} />
+          </TouchableOpacity>
+        )}
+
+        {!data?.date && (
+          <TouchableOpacity style={styles.regionSelector} onPress={() => setCalendarVisible(true)}>
+            <CalendarSvg />
+            <Text style={styles.regionText}>{selectedDate ?? 'Add date'}</Text>
+          </TouchableOpacity>
+        )}
+      </View>
+
+      <FlatList
+        data={imagesStatus}
+        renderItem={renderItem}
+        keyExtractor={(item, index) => index.toString()}
+        numColumns={2}
+        columnWrapperStyle={styles.columnWrapper}
+        showsVerticalScrollIndicator={false}
+      />
+      <Modal
+        onRequestClose={() => setIsModalVisible(false)}
+        headerTitle={'Select Region'}
+        visible={isModalVisible}
+      >
+        <List
+          itemObject={(object) => {
+            setSelectedRegion(object);
+            setIsModalVisible(false);
+          }}
+          initialData={allRegions}
+        />
+      </Modal>
+
+      <RangeCalendar
+        isModalVisible={calendarVisible}
+        closeModal={(date: Date | null) => {
+          date && setSelectedDate(date.toISOString().split('T')[0]);
+          setCalendarVisible(false);
+        }}
+        allowRangeSelection={false}
+      />
+    </PageWrapper>
+  );
+};
+
+export default AddPhotoScreen;

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

@@ -0,0 +1,39 @@
+import { StyleSheet } from 'react-native';
+import { Colors } from 'src/theme';
+
+export const styles = StyleSheet.create({
+  columnWrapper: {
+    justifyContent: 'space-between'
+  },
+  image: {
+    borderRadius: 8
+  },
+  title: { color: '#808080', fontSize: 12, fontWeight: '500', paddingBottom: 16 },
+  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
+  },
+  itemContainer: { alignItems: 'center', marginVertical: 8 },
+  deleteTemp: {
+    position: 'absolute',
+    right: -5,
+    top: -5,
+    padding: 10
+  },
+  btnContainer: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+    gap: 5
+  }
+});

+ 34 - 0
src/screens/InAppScreens/TravelsScreen/Components/CustomButton.tsx

@@ -0,0 +1,34 @@
+import { Text, TouchableOpacity, View } from 'react-native';
+import { styles } from './styles';
+import { Colors } from 'src/theme';
+
+export const CustomButton = ({
+  onPress,
+  title,
+  icon,
+  isActive,
+  disabled
+}: {
+  onPress: () => void;
+  title?: string;
+  icon?: JSX.Element;
+  isActive?: boolean;
+  disabled?: boolean;
+}) => (
+  <TouchableOpacity
+    style={[
+      styles.buttonStyle,
+      isActive ? styles.buttonActive : styles.buttonInactive,
+      { flex: icon && title ? 0.7 : 1, opacity: disabled ? 0.5 : 1 }
+    ]}
+    onPress={onPress}
+    disabled={disabled}
+  >
+    {icon && (
+      <View style={title ? { marginRight: 5 } : { paddingHorizontal: 16, paddingVertical: 8 }}>
+        {icon}
+      </View>
+    )}
+    {title && <Text style={[styles.buttonText, isActive && { color: Colors.WHITE }]}>{title}</Text>}
+  </TouchableOpacity>
+);

+ 145 - 0
src/screens/InAppScreens/TravelsScreen/Components/PhotoEditModal.tsx

@@ -0,0 +1,145 @@
+import React, { useEffect, useState } from 'react';
+import {
+  View,
+  Text,
+  TouchableOpacity,
+  Platform,
+  KeyboardAvoidingView,
+  ScrollView,
+  Image
+} from 'react-native';
+
+import { Button, Input, Modal, FlatList as List } from 'src/components';
+import RangeCalendar from 'src/components/Calendars/RangeCalendar';
+import { ButtonVariants } from 'src/types/components';
+
+import { AllRegions, PhotosData } from '../utils/types';
+import { getImageUri } from '../utils';
+import { styles } from './styles';
+
+import ChevronIcon from '../../../../../assets/icons/chevron-left.svg';
+import CalendarSvg from '../../../../../assets/icons/calendar.svg';
+
+export const PhotoEditModal = ({
+  isVisible,
+  onClose,
+  photo,
+  allRegions,
+  handleUpdate,
+  description,
+  setDescription,
+  selectedDate,
+  setSelectedDate,
+  selectedRegion,
+  setSelectedRegion
+}: {
+  isVisible: boolean;
+  onClose: () => void;
+  photo: PhotosData;
+  allRegions: AllRegions[];
+  handleUpdate: () => void;
+  description: string | null;
+  setDescription: (text: string) => void;
+  selectedDate: string | null;
+  setSelectedDate: (date: string) => void;
+  selectedRegion: AllRegions | null;
+  setSelectedRegion: (region: any) => void;
+}) => {
+  const [imgAspectRatio, setImgAspectRatio] = useState(0);
+  const [isRegionsModalVisible, setIsRegionsModalVisible] = useState(false);
+  const [calendarVisible, setCalendarVisible] = useState(false);
+
+  useEffect(() => {
+    if (photo) {
+      const uri = getImageUri(photo.url_mid);
+      Image.getSize(
+        uri,
+        (width, height) => {
+          const aspectRatio = width / height;
+          setImgAspectRatio(aspectRatio);
+        },
+        (error) => {
+          console.error(`Couldn't get the image size: ${error}`);
+        }
+      );
+    }
+  }, [photo]);
+
+  return (
+    <Modal visible={isVisible} onRequestClose={onClose} headerTitle="Edit photo">
+      <KeyboardAvoidingView
+        behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
+        style={{ flex: 1 }}
+        keyboardVerticalOffset={Platform.select({ ios: 44, android: 27 })}
+      >
+        <ScrollView
+          keyboardShouldPersistTaps="handled"
+          contentContainerStyle={{ flexGrow: 1 }}
+          showsVerticalScrollIndicator={false}
+        >
+          <Image
+            source={{ uri: getImageUri(photo?.url_mid) }}
+            style={[
+              styles.imageEdit,
+              {
+                aspectRatio: imgAspectRatio
+              }
+            ]}
+          />
+          <View style={{ gap: 10, marginTop: 8 }}>
+            <Input
+              placeholder="Photo description"
+              inputMode={'text'}
+              onChange={(text) => setDescription(text)}
+              value={description ?? photo?.title}
+            />
+            <TouchableOpacity
+              style={[styles.regionSelector, , { justifyContent: 'space-between' }]}
+              onPress={() => setIsRegionsModalVisible(true)}
+            >
+              <Text style={styles.regionText}>
+                {selectedRegion?.country ??
+                  allRegions.find((region: AllRegions) => region.id === photo?.region)?.country}
+              </Text>
+              <ChevronIcon fill={'#C8C8C8'} style={{ transform: [{ rotate: '-90deg' }] }} />
+            </TouchableOpacity>
+
+            <TouchableOpacity
+              style={styles.regionSelector}
+              onPress={() => setCalendarVisible(true)}
+            >
+              <CalendarSvg />
+              <Text style={styles.regionText}>{selectedDate ?? photo?.date}</Text>
+            </TouchableOpacity>
+
+            <RangeCalendar
+              isModalVisible={calendarVisible}
+              closeModal={(date: Date | null) => {
+                date && setSelectedDate(date.toISOString().split('T')[0]);
+                setCalendarVisible(false);
+              }}
+              allowRangeSelection={false}
+            />
+          </View>
+          <View style={styles.saveBtn}>
+            <Button variant={ButtonVariants.FILL} onPress={handleUpdate} children={'Save'} />
+          </View>
+
+          <Modal
+            onRequestClose={() => setIsRegionsModalVisible(false)}
+            headerTitle={'Select Region'}
+            visible={isRegionsModalVisible}
+          >
+            <List
+              itemObject={(object) => {
+                setSelectedRegion(object);
+                setIsRegionsModalVisible(false);
+              }}
+              initialData={allRegions}
+            />
+          </Modal>
+        </ScrollView>
+      </KeyboardAvoidingView>
+    </Modal>
+  );
+};

+ 103 - 0
src/screens/InAppScreens/TravelsScreen/Components/PhotoItem.tsx

@@ -0,0 +1,103 @@
+import React, { useCallback, useMemo } from 'react';
+import { View, Image, Text, TouchableOpacity } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+
+import { getImageUri, photoStyles, smallPhotoHeight, smallPhotoWidth } from '../utils';
+import { AllRegions, PhotosData } from '../utils/types';
+import { NAVIGATION_PAGES } from 'src/types';
+import { styles } from './styles';
+
+import ChevronRightIcon from '../../../../../assets/icons/travels-screens/chevron-right.svg';
+
+export const PhotoItem = React.memo(
+  ({ item, allRegions }: { item: any; allRegions: AllRegions[] }) => {
+    const navigation = useNavigation();
+
+    const renderPhotos = () => {
+      const photos = item.photos.slice(0, 3);
+      const morePhotosCount = item.photos.length - 3;
+
+      if (photos.length === 1) {
+        return (
+          <Image source={{ uri: getImageUri(photos[0].url_small) }} style={photoStyles.onePhoto} />
+        );
+      } else if (photos.length === 2) {
+        return photos.map((photo: PhotosData, index: number) => (
+          <Image
+            key={photo.id}
+            source={{ uri: getImageUri(photo.url_small) }}
+            style={[photoStyles.twoPhotos, index === 0 && { marginRight: 8 }]}
+          />
+        ));
+      } else {
+        return (
+          <View style={{ flexDirection: 'row' }}>
+            <Image
+              source={{ uri: getImageUri(photos[0].url_small) }}
+              style={photoStyles.bigPhoto}
+            />
+            <View style={styles.smallPhotoContainer}>
+              <Image
+                source={{ uri: getImageUri(photos[1].url_small) }}
+                style={photoStyles.smallPhoto}
+              />
+              <Image
+                source={{ uri: getImageUri(photos[2].url_small) }}
+                style={[photoStyles.smallPhoto, { position: 'relative' }]}
+              />
+              {morePhotosCount > 0 && (
+                <View
+                  style={[
+                    styles.morePhotosOverlay,
+                    { width: smallPhotoWidth, height: smallPhotoHeight }
+                  ]}
+                >
+                  <Text style={styles.morePhotosText}>+{morePhotosCount}</Text>
+                </View>
+              )}
+            </View>
+          </View>
+        );
+      }
+    };
+
+    const photoViews = useMemo(() => renderPhotos(), [item.photos]);
+
+    const handlePress = useCallback(() => {
+      navigation.navigate(
+        ...([
+          NAVIGATION_PAGES.MORE_PHOTOS,
+          {
+            data: item,
+            allRegions: allRegions
+          }
+        ] as never)
+      );
+    }, [navigation, item]);
+
+    return (
+      <View style={styles.listItem}>
+        <View style={[styles.listItemHeader, item.date && { alignItems: 'flex-end' }]}>
+          {item.date ? (
+            <View style={styles.listItemTextContainer}>
+              <Text style={styles.listItemTitle}>{item.date}</Text>
+            </View>
+          ) : (
+            <>
+              <Image style={styles.flagImg} source={{ uri: getImageUri(item.flag) }} />
+              <View style={styles.listItemTextContainer}>
+                <Text style={styles.listItemTitle}>{item.country}</Text>
+                {item.name && <Text style={styles.listItemSubtitle}>{item.name}</Text>}
+              </View>
+            </>
+          )}
+          <TouchableOpacity style={styles.moreButton} onPress={handlePress}>
+            <Text style={styles.moreText}>More </Text>
+            <ChevronRightIcon />
+          </TouchableOpacity>
+        </View>
+        <View style={styles.photoContainer}>{photoViews}</View>
+      </View>
+    );
+  }
+);

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

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

+ 120 - 1
src/screens/InAppScreens/TravelsScreen/Components/styles.tsx

@@ -1,5 +1,6 @@
 import { StyleSheet, Dimensions } from 'react-native';
 import { Colors } from '../../../../theme';
+import { getFontSize } from 'src/utils';
 
 export const styles = StyleSheet.create({
   header: {
@@ -87,5 +88,123 @@ export const styles = StyleSheet.create({
     height: '100%',
     alignItems: 'center',
     justifyContent: 'center'
-  }
+  },
+  buttonStyle: {
+    flexDirection: 'row',
+    justifyContent: 'center',
+    alignItems: 'center',
+    borderRadius: 20,
+    marginHorizontal: 2,
+    paddingVertical: 10
+  },
+  buttonActive: {
+    backgroundColor: Colors.ORANGE,
+    borderColor: Colors.ORANGE,
+    borderWidth: 1
+  },
+  buttonInactive: {
+    backgroundColor: 'transparent',
+    borderColor: 'rgba(15, 63, 79, 0.3)',
+    borderWidth: 1
+  },
+  buttonText: {
+    color: Colors.DARK_BLUE,
+    fontWeight: '600',
+    fontSize: getFontSize(12)
+  },
+  smallPhotoContainer: {
+    marginLeft: 8,
+    justifyContent: 'space-between'
+  },
+  morePhotosOverlay: {
+    backgroundColor: 'rgba(0, 0, 0, 0.45)',
+    justifyContent: 'center',
+    alignItems: 'center',
+    borderRadius: 8,
+    position: 'absolute',
+    bottom: 0,
+    left: 0
+  },
+  morePhotosText: {
+    color: Colors.WHITE,
+    fontSize: 22,
+    fontWeight: 'bold'
+  },
+  listItem: {
+    flexDirection: 'column',
+    alignItems: 'flex-start',
+    paddingVertical: 16
+  },
+  listItemHeader: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    width: '100%',
+    marginBottom: 8
+  },
+  listItemTextContainer: {
+    flex: 1,
+    justifyContent: 'center'
+  },
+  listItemTitle: {
+    fontSize: 14,
+    fontWeight: 'bold',
+    color: Colors.DARK_BLUE
+  },
+  listItemSubtitle: {
+    fontSize: 12,
+    color: '#808080',
+    fontWeight: '500'
+  },
+  flagImg: {
+    borderRadius: 18,
+    overflow: 'hidden',
+    marginRight: 8,
+    width: 36,
+    height: 36,
+    resizeMode: 'cover',
+    borderWidth: 1,
+    borderColor: '#F4F4F4'
+  },
+  moreButton: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'center',
+    borderWidth: 1,
+    borderColor: Colors.DARK_BLUE,
+    borderRadius: 15,
+    paddingHorizontal: 12,
+    paddingVertical: 8
+  },
+  moreText: {
+    fontSize: 12,
+    textAlign: 'center',
+    color: Colors.DARK_BLUE,
+    fontWeight: 'bold'
+  },
+  photoContainer: {
+    flexDirection: 'row',
+    marginTop: 8
+  },
+  imageEdit: {
+    width: '100%',
+    height: undefined,
+    resizeMode: 'contain',
+    borderRadius: 8,
+    marginTop: 8
+  },
+  saveBtn: { justifyContent: 'flex-end', flex: 1, marginBottom: 24, marginTop: 16 },
+  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
+  },
 });

+ 392 - 0
src/screens/InAppScreens/TravelsScreen/MorePhotosScreen/index.tsx

@@ -0,0 +1,392 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import { View, FlatList, Image, Text, TouchableOpacity } from 'react-native';
+import ImageView from 'react-native-image-viewing';
+import ReactModal from 'react-native-modal';
+import * as ImagePicker from 'expo-image-picker';
+import { useNavigation } from '@react-navigation/native';
+
+import { Header, Loading, PageWrapper, WarningModal } from 'src/components';
+import { CustomButton, PhotoEditModal } from '../Components';
+import {
+  useGetTempQuery,
+  usePostDeletePhotoMutation,
+  usePostUpdatePhotoMutation
+} from '@api/photos';
+
+import { API_HOST } from 'src/constants';
+import { Colors } from 'src/theme';
+import { NAVIGATION_PAGES } from 'src/types';
+import { StoreType, storage } from 'src/storage';
+import { AllRegions, PhotosData } from '../utils/types';
+import { itemWidth } from '../utils';
+import { styles } from './styles';
+
+import AddImgSvg from '../../../../../assets/icons/travels-screens/add-img.svg';
+import ChooseSvg from '../../../../../assets/icons/travels-screens/choose.svg';
+import DotsSvg from '../../../../../assets/icons/travels-screens/three-dots.svg';
+import TrashSvg from '../../../../../assets/icons/travels-screens/trash-solid.svg';
+import CircleCheckSvg from '../../../../../assets/icons/travels-screens/circle-check.svg';
+import PenSvg from '../../../../../assets/icons/travels-screens/pen-to-square.svg';
+
+const MorePhotosScreen = ({ route }: { route: any }) => {
+  const { data, allRegions } = route.params;
+  const token = storage.get('token', StoreType.STRING) as string;
+  const { data: tempData, refetch } = useGetTempQuery(token, true);
+  const { mutate: deletePhoto } = usePostDeletePhotoMutation();
+  const { mutate: updatePhoto } = usePostUpdatePhotoMutation();
+  const navigation = useNavigation();
+
+  const [selectionMode, setSelectionMode] = useState(false);
+  const [selectedPhotos, setSelectedPhotos] = useState<{ [key: string]: boolean }>({});
+  const [photoToEdit, setPhotoToEdit] = useState<PhotosData | null>(null);
+  const [isLoading, setIsLoading] = useState(false);
+  const [photos, setPhotos] = useState<PhotosData[]>(data.photos ?? []);
+  const [shouldOpenWarningModal, setShouldOpenWarningModal] = useState(false);
+  const [shouldOpenEditModal, setShouldOpenEditModal] = useState(false);
+  const [selectedDate, setSelectedDate] = useState<string | null>(null);
+  const [description, setDescription] = useState<string | null>(null);
+  const [selectedRegion, setSelectedRegion] = useState<AllRegions | null>(null);
+  const [fullImages, setFullImages] = useState<PhotosData[]>([]);
+  const [currentImageIndex, setCurrentImageIndex] = useState(0);
+  const [modalState, setModalState] = useState({
+    isModalVisible: false,
+    isWarningVisible: false,
+    isEditModalVisible: false,
+    isViewerVisible: false
+  });
+
+  const handleSelectPhoto = useCallback((id: number) => {
+    setSelectedPhotos((prevSelected) => {
+      const isSelected = !!prevSelected[id];
+      return {
+        ...prevSelected,
+        [id]: !isSelected
+      };
+    });
+  }, []);
+
+  const toggleSelectionMode = () => {
+    setSelectionMode(!selectionMode);
+  };
+
+  useEffect(() => {
+    setFullImages(
+      photos.map((photo) => ({
+        ...photo,
+        uri: API_HOST + photo.url_mid
+      }))
+    );
+  }, [photos]);
+
+  const openImageViewer = (index: number) => {
+    setCurrentImageIndex(index);
+    openModal('isViewerVisible');
+  };
+
+  const deleteSelectedPhotos = () => {
+    const photoIds = Object.entries(selectedPhotos)
+      .filter(([key, value]) => value)
+      .map(([key, value]) => +key);
+    photoIds.forEach((id) => {
+      deletePhoto({ token, photo_id: id });
+    });
+    setPhotos(photos.filter((photo) => !photoIds.includes(photo.id)));
+    setSelectedPhotos({});
+  };
+
+  const cancelSelection = () => {
+    setSelectedPhotos({});
+    setSelectionMode(false);
+  };
+
+  const handleDeletePhoto = () => {
+    photoToEdit &&
+      deletePhoto(
+        { token, photo_id: photoToEdit.id },
+        {
+          onSuccess: (res) => {
+            if (res.result === 'OK') {
+              setPhotos(photos.filter((photo) => photo.id !== photoToEdit.id));
+            }
+          }
+        }
+      );
+    setPhotoToEdit(null);
+    closeModal('isModalVisible');
+  };
+
+  const handleUpdatePhoto = () => {
+    photoToEdit &&
+      updatePhoto(
+        {
+          token,
+          photo_id: photoToEdit.id,
+          date: selectedDate,
+          region: selectedRegion?.id ?? null,
+          description: description ?? photoToEdit.title
+        },
+        {
+          onSuccess: (res) => {
+            if (res.result === 'OK') {
+              const filteredPhotos = photos.filter((photo) => {
+                if (selectedRegion && selectedRegion?.id !== data?.region) {
+                  return photo.id !== photoToEdit.id;
+                } else if (selectedDate && selectedDate !== data?.date) {
+                  return photo.id !== photoToEdit.id;
+                } else {
+                  return true;
+                }
+              });
+
+              const updatedPhotos =
+                typeof description === 'string'
+                  ? filteredPhotos.map((photo) => {
+                      if (photo.id === photoToEdit.id) {
+                        return { ...photo, title: description };
+                      }
+                      return photo;
+                    })
+                  : filteredPhotos;
+
+              setPhotos(updatedPhotos);
+            }
+          }
+        }
+      );
+
+    closeModal('isEditModalVisible');
+    setSelectedDate(null);
+    setSelectedRegion(null);
+    setDescription(null);
+  };
+
+  const renderItem = ({ item, index }: { item: PhotosData; index: number }) => (
+    <View style={{ alignItems: 'center', marginVertical: 8 }}>
+      <TouchableOpacity
+        style={{ position: 'relative' }}
+        onPress={() => (selectionMode ? handleSelectPhoto(item.id) : openImageViewer(index))}
+      >
+        <Image
+          source={{ uri: API_HOST + item?.url_small }}
+          style={[styles.image, { width: itemWidth, height: itemWidth }]}
+        />
+        <TouchableOpacity
+          style={styles.btnDots}
+          onPress={() => {
+            setPhotoToEdit(item);
+            openModal('isModalVisible');
+          }}
+        >
+          <DotsSvg />
+        </TouchableOpacity>
+        {selectionMode && (
+          <View
+            style={[
+              styles.selectionIndicator,
+              { backgroundColor: selectedPhotos[item.id] ? Colors.WHITE : 'rgba(15, 63, 79, 0.3)' }
+            ]}
+          >
+            {selectedPhotos[item.id] && (
+              <CircleCheckSvg fill={Colors.ORANGE} height={22} width={22} />
+            )}
+          </View>
+        )}
+      </TouchableOpacity>
+      {item.title && (
+        <Text style={[styles.description, { maxWidth: itemWidth }]}>{item.title}</Text>
+      )}
+    </View>
+  );
+
+  const handleImagePick = async () => {
+    setIsLoading(true);
+
+    await refetch().then(async (temp) => {
+      if (temp.data?.photos && temp.data?.photos.length > 0) {
+        setIsLoading(false);
+        navigation.navigate(
+          ...([
+            NAVIGATION_PAGES.ADD_PHOTO,
+            {
+              data: data,
+              images: [],
+              allRegions: allRegions,
+              tempData: temp.data.photos
+            }
+          ] as never)
+        );
+      } else {
+        setIsLoading(false);
+
+        let result = await ImagePicker.launchImageLibraryAsync({
+          mediaTypes: ImagePicker.MediaTypeOptions.Images,
+          quality: 1,
+          allowsMultipleSelection: true,
+          exif: true
+        });
+
+        if (!result.canceled) {
+          navigation.navigate(
+            ...([
+              NAVIGATION_PAGES.ADD_PHOTO,
+              {
+                data: data,
+                images: result.assets,
+                allRegions: allRegions
+              }
+            ] as never)
+          );
+        }
+      }
+    });
+  };
+
+  const handleEditModalClose = () => {
+    closeModal('isEditModalVisible');
+    setSelectedDate(null);
+    setSelectedRegion(null);
+    setDescription(null);
+  };
+
+  const closeModal = (modalName: string) => {
+    setModalState((prevState) => ({ ...prevState, [modalName]: false }));
+  };
+
+  const openModal = (modalName: string) => {
+    setModalState((prevState) => ({ ...prevState, [modalName]: true }));
+  };
+
+  return (
+    <PageWrapper style={{ position: 'relative' }}>
+      <Header label={data.country ?? data.date} />
+      {isLoading && <Loading />}
+      <View style={{ alignItems: 'center', marginBottom: 8 }}>
+        {data.name && <Text style={styles.title}>{data.name}</Text>}
+        <View
+          style={[
+            styles.btnContainer,
+            {
+              width: selectionMode ? 'auto' : '85%'
+            }
+          ]}
+        >
+          {selectionMode ? (
+            <>
+              <View>
+                <CustomButton
+                  onPress={() => openModal('isWarningVisible')}
+                  icon={<TrashSvg fill={Colors.DARK_BLUE} />}
+                />
+              </View>
+              <CustomButton
+                title="Cancel"
+                onPress={cancelSelection}
+                icon={<CircleCheckSvg fill={Colors.DARK_BLUE} />}
+              />
+            </>
+          ) : (
+            <CustomButton
+              title="Choose"
+              icon={<ChooseSvg fill={Colors.DARK_BLUE} />}
+              onPress={toggleSelectionMode}
+            />
+          )}
+          <CustomButton
+            title="Add photo"
+            icon={<AddImgSvg fill={Colors.DARK_BLUE} />}
+            onPress={handleImagePick}
+          />
+        </View>
+      </View>
+      <FlatList
+        data={photos}
+        renderItem={renderItem}
+        keyExtractor={(item) => item.id.toString()}
+        numColumns={2}
+        columnWrapperStyle={styles.columnWrapper}
+        showsVerticalScrollIndicator={false}
+      />
+      <ReactModal
+        isVisible={modalState.isModalVisible}
+        onBackdropPress={() => closeModal('isModalVisible')}
+        style={styles.modal}
+        statusBarTranslucent={true}
+        presentationStyle="overFullScreen"
+        onModalHide={() => {
+          if (shouldOpenWarningModal) {
+            openModal('isWarningVisible');
+            setShouldOpenWarningModal(false);
+          } else if (shouldOpenEditModal) {
+            openModal('isEditModalVisible');
+            setShouldOpenEditModal(false);
+          }
+        }}
+      >
+        <View style={styles.wrapper}>
+          <View style={{ paddingVertical: 36 }}>
+            <TouchableOpacity
+              style={styles.btnModalEdit}
+              onPress={() => {
+                closeModal('isModalVisible');
+                setShouldOpenEditModal(true);
+              }}
+            >
+              <PenSvg fill={Colors.DARK_BLUE} />
+              <Text style={styles.btnModalEditText}>Edit photo</Text>
+            </TouchableOpacity>
+
+            <TouchableOpacity
+              style={styles.btnModalEdit}
+              onPress={() => {
+                closeModal('isModalVisible');
+                setShouldOpenWarningModal(true);
+              }}
+            >
+              <TrashSvg fill={Colors.DARK_BLUE} />
+              <Text style={styles.btnModalEditText}>Delete</Text>
+            </TouchableOpacity>
+          </View>
+        </View>
+      </ReactModal>
+      <WarningModal
+        isVisible={modalState.isWarningVisible}
+        onClose={() => {
+          closeModal('isWarningVisible');
+          setPhotoToEdit(null);
+        }}
+        type="delete"
+        title={photoToEdit ? 'Delete photo?' : 'Delete photos?'}
+        message={
+          photoToEdit
+            ? 'Are you sure you want to delete this photo?'
+            : 'Are you sure you want to delete these photos?'
+        }
+        action={photoToEdit ? handleDeletePhoto : deleteSelectedPhotos}
+      />
+      <PhotoEditModal
+        isVisible={modalState.isEditModalVisible}
+        onClose={handleEditModalClose}
+        photo={photoToEdit as PhotosData}
+        allRegions={allRegions}
+        handleUpdate={handleUpdatePhoto}
+        description={description}
+        setDescription={setDescription}
+        selectedDate={selectedDate}
+        setSelectedDate={setSelectedDate}
+        selectedRegion={selectedRegion}
+        setSelectedRegion={setSelectedRegion}
+      />
+      <ImageView
+        images={fullImages}
+        keyExtractor={(imageSrc, index) => index.toString()}
+        imageIndex={currentImageIndex}
+        visible={modalState.isViewerVisible}
+        onRequestClose={() => closeModal('isViewerVisible')}
+        swipeToCloseEnabled={false}
+        backgroundColor="transparent"
+      />
+    </PageWrapper>
+  );
+};
+
+export default MorePhotosScreen;

+ 66 - 0
src/screens/InAppScreens/TravelsScreen/MorePhotosScreen/styles.tsx

@@ -0,0 +1,66 @@
+import { StyleSheet } from 'react-native';
+import { Colors } from 'src/theme';
+
+export const styles = StyleSheet.create({
+  columnWrapper: {
+    justifyContent: 'space-between'
+  },
+  image: {
+    borderRadius: 8
+  },
+  selectionIndicator: {
+    height: 24,
+    width: 24,
+    borderRadius: 12,
+    position: 'absolute',
+    right: 10,
+    bottom: 10,
+    borderColor: Colors.WHITE,
+    borderWidth: 1,
+    alignItems: 'center',
+    justifyContent: 'center',
+    alignContent: 'center'
+  },
+  wrapper: {
+    backgroundColor: Colors.WHITE,
+    paddingLeft: 15,
+    paddingRight: 15,
+    borderTopLeftRadius: 10,
+    borderTopRightRadius: 10,
+    height: 'auto'
+  },
+  modal: {
+    justifyContent: 'flex-end',
+    margin: 0
+  },
+  title: { color: '#808080', fontSize: 12, fontWeight: '500', paddingBottom: 16 },
+  btnDots: {
+    height: 36,
+    width: 36,
+    borderRadius: 18,
+    backgroundColor: 'rgba(33, 37, 41, 0.6)',
+    position: 'absolute',
+    right: 5,
+    top: 5,
+    padding: 8
+  },
+  description: {
+    color: Colors.DARK_BLUE,
+    fontSize: 12,
+    paddingTop: 2
+  },
+  btnContainer: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center',
+    gap: 15
+  },
+  btnModalEdit: {
+    paddingHorizontal: 16,
+    paddingVertical: 8,
+    flexDirection: 'row',
+    alignItems: 'center',
+    gap: 12
+  },
+  btnModalEditText: { fontSize: 12, fontWeight: '600', color: Colors.DARK_BLUE },
+});

+ 248 - 0
src/screens/InAppScreens/TravelsScreen/PhotosScreen/index.tsx

@@ -0,0 +1,248 @@
+import React, { useCallback, useEffect, useState } from 'react';
+import { View, Text, TouchableOpacity, FlatList } from 'react-native';
+import { useFocusEffect, useNavigation } from '@react-navigation/native';
+import * as ImagePicker from 'expo-image-picker';
+
+import { PageWrapper, Header, Modal, FlatList as List, Loading } from 'src/components';
+import { PhotoItem, CustomButton } from '../Components';
+import { useGetPhotosForUserQuery, useGetTempQuery } from '@api/photos';
+import { StoreType, storage } from 'src/storage';
+
+import { AllRegions, ByDateData, ByRegionData, PhotosData } from '../utils/types';
+import { groupByDate } from '../utils';
+import { NAVIGATION_PAGES } from 'src/types';
+import { Colors } from 'src/theme';
+import { styles } from './styles';
+
+import AddImgSvg from '../../../../../assets/icons/travels-screens/add-img.svg';
+import ChevronIcon from '../../../../../assets/icons/chevron-left.svg';
+
+const ByRegionContent = ({
+  data,
+  allRegions
+}: {
+  data: ByRegionData[];
+  allRegions: AllRegions[];
+}) => {
+  const [loading, setLoading] = useState(true);
+  const [visible, setVisible] = useState(false);
+  const [selectedRegion, setSelectedRegion] = useState<ByRegionData>();
+  const [photos, setPhotos] = useState<ByRegionData[]>([]);
+
+  useEffect(() => {
+    if (selectedRegion?.country === 'All Regions') {
+      setPhotos(data);
+    } else {
+      setPhotos(
+        data.filter(
+          (item) => item.country === selectedRegion?.country && item.name === selectedRegion?.name
+        )
+      );
+    }
+  }, [selectedRegion]);
+
+  useFocusEffect(
+    useCallback(() => {
+      setPhotos(data);
+      setLoading(false);
+    }, [data])
+  );
+
+  if (loading) return <Loading />;
+
+  return (
+    <View style={{ flex: 1 }}>
+      <TouchableOpacity style={styles.regionSelector} onPress={() => setVisible(true)}>
+        <Text style={styles.regionText}>{selectedRegion?.country ?? 'All Regions'}</Text>
+        <ChevronIcon fill={'#C8C8C8'} style={{ transform: [{ rotate: '-90deg' }] }} />
+      </TouchableOpacity>
+      <Modal
+        onRequestClose={() => setVisible(false)}
+        headerTitle={'Select Region'}
+        visible={visible}
+      >
+        <List
+          itemObject={(object) => {
+            setVisible(false);
+            setSelectedRegion(object);
+          }}
+          initialData={[{ country: 'All Regions', id: 0 }, ...data]}
+        />
+      </Modal>
+
+      <FlatList
+        data={photos}
+        renderItem={({ item }) => <PhotoItem item={item} allRegions={allRegions} />}
+        keyExtractor={(item) => item.id.toString()}
+        style={styles.listContainer}
+        showsVerticalScrollIndicator={false}
+      />
+    </View>
+  );
+};
+
+const ByDateContent = ({ data, allRegions }: { data: ByDateData; allRegions: AllRegions[] }) => {
+  const [loading, setLoading] = useState<boolean>(true);
+  const [visible, setVisible] = useState<boolean>(false);
+  const [selectedDate, setSelectedDate] = useState<string | null>(null);
+  const [photos, setPhotos] = useState<{ date: string; photos: PhotosData[] }[]>([]);
+  const [filter, setFilter] = useState<string[]>([]);
+
+  useEffect(() => {
+    if (selectedDate) {
+      setPhotos(groupByDate(data[selectedDate]));
+    }
+  }, [selectedDate]);
+
+  useFocusEffect(
+    useCallback(() => {
+      const keys: string[] = Object.keys(data);
+
+      if (keys.length > 0) {
+        const firstKey = keys[0];
+
+        const groupedByDate = groupByDate(data[firstKey]);
+        setPhotos(groupedByDate);
+        setFilter(keys);
+      }
+      setLoading(false);
+    }, [data])
+  );
+
+  if (loading) return <Loading />;
+
+  return (
+    <View style={{ flex: 1 }}>
+      <TouchableOpacity style={styles.regionSelector} onPress={() => setVisible(true)}>
+        <Text style={styles.regionText}>{selectedDate ?? filter[0]}</Text>
+        <ChevronIcon fill={'#C8C8C8'} style={{ transform: [{ rotate: '-90deg' }] }} />
+      </TouchableOpacity>
+      <Modal onRequestClose={() => setVisible(false)} headerTitle={'Select Date'} visible={visible}>
+        <List
+          itemObject={(object) => {
+            setVisible(false);
+            setSelectedDate(object);
+          }}
+          initialData={filter}
+          date={true}
+        />
+      </Modal>
+
+      <FlatList
+        data={photos}
+        renderItem={({ item }) => <PhotoItem item={item} allRegions={allRegions} />}
+        keyExtractor={(item) => item.date}
+        style={styles.listContainer}
+        showsVerticalScrollIndicator={false}
+      />
+    </View>
+  );
+};
+
+const PhotosScreen = () => {
+  const token = storage.get('token', StoreType.STRING) as string;
+  const [loading, setLoading] = useState(true);
+  const [contentIndex, setContentIndex] = useState(0);
+  const { data, refetch } = useGetPhotosForUserQuery(token, true);
+  const [allRegions, setAllRegions] = useState<AllRegions[]>([]);
+  const { data: tempData, refetch: refetchTemp } = useGetTempQuery(token, true);
+  const navigation = useNavigation();
+
+  useFocusEffect(
+    useCallback(() => {
+      const fetchData = async () => {
+        setLoading(true);
+        try {
+          await refetch().then((res) => {
+            if (res.data?.result === 'OK') {
+              setAllRegions(res?.data?.all_regions);
+            }
+          });
+        } catch (error) {
+          console.error(error);
+        } finally {
+          setLoading(false);
+        }
+      };
+
+      fetchData();
+    }, [refetch])
+  );
+
+  if (loading) return <Loading />;
+
+  const renderContent = () => {
+    switch (contentIndex) {
+      case 0:
+        return <ByRegionContent data={data?.by_region as ByRegionData[]} allRegions={allRegions} />;
+      case 1:
+        return <ByDateContent data={data?.by_date as ByDateData} allRegions={allRegions} />;
+      default:
+        return null;
+    }
+  };
+
+  const handleImagePick = async () => {
+    await refetchTemp().then(async (temp) => {
+      if (temp.data?.photos && temp.data.photos.length > 0) {
+        navigation.navigate(
+          ...([
+            NAVIGATION_PAGES.ADD_PHOTO,
+            {
+              data: null,
+              images: [],
+              allRegions: allRegions,
+              tempData: temp.data.photos
+            }
+          ] as never)
+        );
+      } else {
+        let result = await ImagePicker.launchImageLibraryAsync({
+          mediaTypes: ImagePicker.MediaTypeOptions.Images,
+          quality: 1,
+          allowsMultipleSelection: true,
+          exif: true
+        });
+
+        if (!result.canceled) {
+          navigation.navigate(
+            ...([
+              NAVIGATION_PAGES.ADD_PHOTO,
+              {
+                data: null,
+                images: result.assets,
+                allRegions: allRegions
+              }
+            ] as never)
+          );
+        }
+      }
+    });
+  };
+
+  return (
+    <PageWrapper>
+      <Header label="Photos" />
+      <View style={styles.buttonContainer}>
+        <CustomButton
+          title="By region"
+          onPress={() => setContentIndex(0)}
+          isActive={contentIndex === 0}
+        />
+        <CustomButton
+          title="By date"
+          onPress={() => setContentIndex(1)}
+          isActive={contentIndex === 1}
+        />
+        <CustomButton
+          title="Add"
+          icon={<AddImgSvg fill={Colors.DARK_BLUE} />}
+          onPress={handleImagePick}
+        />
+      </View>
+      {renderContent()}
+    </PageWrapper>
+  );
+};
+
+export default PhotosScreen;

+ 27 - 0
src/screens/InAppScreens/TravelsScreen/PhotosScreen/styles.tsx

@@ -0,0 +1,27 @@
+import { StyleSheet } from 'react-native';
+import { Colors } from 'src/theme';
+
+export const styles = StyleSheet.create({
+  buttonContainer: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center'
+  },
+  regionSelector: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between',
+    paddingHorizontal: 16,
+    backgroundColor: 'rgba(250, 250, 250, 1)',
+    borderRadius: 4,
+    marginTop: 16,
+    height: 34
+  },
+  regionText: {
+    fontSize: 15,
+    color: Colors.LIGHT_GRAY
+  },
+  listContainer: {
+    marginTop: 10
+  },
+});

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

@@ -84,7 +84,6 @@ const SeriesScreen = () => {
 };
 
 const SeriesList = React.memo(({ groupId, navigation }: { groupId: string, navigation: any }) => {
-  // const navigation = useNavigation();
   const [seriesData, setSeriesData] = useState<SeriesList[]>([]);
   const [isLoading, setIsLoading] = useState(true);
 

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

@@ -60,7 +60,14 @@ const TravelsScreen = () => {
     },
     {
       label: 'Photos',
-      icon: <ImagesIcon fill={Colors.DARK_BLUE} width={20} height={20} />
+      icon: <ImagesIcon fill={Colors.DARK_BLUE} width={20} height={20} />,
+      buttonFn: (navigation) => {
+        if (!token) {
+          setIsModalVisible(true);
+        } else {
+          navigation.navigate(NAVIGATION_PAGES.PHOTOS);
+        }
+      }
     }
   ];
 

+ 19 - 0
src/screens/InAppScreens/TravelsScreen/utils/groupPhotosByDate.tsx

@@ -0,0 +1,19 @@
+import { PhotosData } from './types';
+
+export function groupByDate(items: PhotosData[]) {
+  const groupedByDate = items.reduce((acc: { [key: string]: PhotosData[] }, item) => {
+    const { date } = item;
+    if (!acc[date]) {
+      acc[date] = [];
+    }
+    acc[date].push(item);
+    return acc;
+  }, {});
+
+  return Object.entries(groupedByDate)
+    .map(([date, photos]) => ({
+      date: date,
+      photos: photos as PhotosData[]
+    }))
+    .sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());
+}

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

@@ -0,0 +1,2 @@
+export * from './photoStyles'
+export * from './groupPhotosByDate'

+ 38 - 0
src/screens/InAppScreens/TravelsScreen/utils/photoStyles.ts

@@ -0,0 +1,38 @@
+import { Dimensions } from 'react-native';
+import { API_HOST } from 'src/constants';
+
+const windowWidth = Dimensions.get('window').width;
+const pageMargin = windowWidth * 0.05;
+const usableWidth = windowWidth - pageMargin * 2;
+const photoMargin = 8;
+const twoPhotos = (usableWidth - photoMargin) / 2;
+const bigPhotoWidth = ((usableWidth - photoMargin) * 2) / 3;
+
+export const smallPhotoWidth = (usableWidth - photoMargin) / 3;
+export const smallPhotoHeight = (bigPhotoWidth - photoMargin) / 2;
+export const itemWidth = (usableWidth - 16) / 2;
+
+export const photoStyles = {
+  onePhoto: {
+    width: usableWidth,
+    height: usableWidth,
+    borderRadius: 8
+  },
+  twoPhotos: {
+    width: twoPhotos,
+    height: twoPhotos,
+    borderRadius: 8
+  },
+  bigPhoto: {
+    width: bigPhotoWidth,
+    height: bigPhotoWidth,
+    borderRadius: 8
+  },
+  smallPhoto: {
+    width: smallPhotoWidth,
+    height: smallPhotoHeight,
+    borderRadius: 8
+  }
+};
+
+export const getImageUri = (path: string) => `${API_HOST}${path}`;

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

@@ -0,0 +1,51 @@
+export interface ByDateData {
+  [key: string]: PhotosData[];
+}
+
+export interface ByRegionData {
+  id: number;
+  country: string;
+  name: string;
+  flag: string;
+  nm: boolean;
+  dare: boolean;
+  photos: PhotosData[];
+}
+
+export interface PhotosData {
+  id: number;
+  region_nm: number;
+  region_dare: number;
+  region: number;
+  title: string;
+  date: string;
+  url_small: string;
+  url_mid: string;
+  url_full: string;
+  album_id: number;
+  album_name: string;
+  uri?: string;
+}
+
+export interface AllRegions {
+  id: number;
+  dare: boolean;
+  nm: boolean;
+  country: string;
+  name: string;
+  flag: string;
+}
+
+export interface TempData {
+  guid: string;
+  link: string;
+}
+
+export interface ImageStatus {
+  uri?: string;
+  assetId: string;
+  loaded: boolean;
+  uploadResponse: TempData | null;
+  dateTime: string | null;
+  progress: number;
+}

+ 18 - 3
src/types/api.ts

@@ -8,7 +8,8 @@ export enum API_ROUTE {
   UN_MASTERS = 'un-masters',
   AVATARS = 'avatars',
   STATISTICS = 'statistics',
-  KYE = 'kye'
+  KYE = 'kye',
+  PHOTOS = 'photos'
 }
 
 export enum API_ENDPOINT {
@@ -41,7 +42,14 @@ export enum API_ENDPOINT {
   GET_ITEMS_FOR_SERIES = 'get-items-for-series-grouped-app',
   TOGGLE_ITEM_SERIES = 'toggle-item',
   GET_KYE = 'get-kye',
-  SET_KYE = 'set-kye'
+  SET_KYE = 'set-kye',
+  GET_PHOTOS_FOR_USER = 'get-photos-for-user',
+  UPLOAD_TEMP = 'upload-temp',
+  GET_TEMP = 'get-temp',
+  REMOVE_TEMP = 'remove-temp',
+  SAVE_TEMP = 'save-temp',
+  DELETE_PHOTO = 'delete-photo',
+  UPDATE_PHOTO = 'update-photo'
 }
 
 export enum API {
@@ -73,7 +81,14 @@ export enum API {
   GET_ITEMS_FOR_SERIES = `${API_ROUTE.SERIES}/${API_ENDPOINT.GET_ITEMS_FOR_SERIES}`,
   TOGGLE_ITEM_SERIES = `${API_ROUTE.SERIES}/${API_ENDPOINT.TOGGLE_ITEM_SERIES}`,
   GET_KYE = `${API_ROUTE.KYE}/${API_ENDPOINT.GET_KYE}`,
-  SET_KYE = `${API_ROUTE.KYE}/${API_ENDPOINT.SET_KYE}`
+  SET_KYE = `${API_ROUTE.KYE}/${API_ENDPOINT.SET_KYE}`,
+  GET_PHOTOS_FOR_USER = `${API_ROUTE.PHOTOS}/${API_ENDPOINT.GET_PHOTOS_FOR_USER}`,
+  UPLOAD_TEMP = `${API_ROUTE.PHOTOS}/${API_ENDPOINT.UPLOAD_TEMP}`,
+  GET_TEMP = `${API_ROUTE.PHOTOS}/${API_ENDPOINT.GET_TEMP}`,
+  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}`
 }
 
 export type BaseAxiosError = AxiosError;

+ 3 - 0
src/types/navigation.ts

@@ -25,4 +25,7 @@ export enum NAVIGATION_PAGES {
   SERIES = 'inAppSeries',
   SERIES_ITEM = 'inAppSeriesItem',
   EARTH = 'inAppEarth',
+  PHOTOS = 'inAppPhotos',
+  MORE_PHOTOS = 'inAppMorePhotos',
+  ADD_PHOTO = 'inAppAddPhoto',
 }

Некоторые файлы не были показаны из-за большого количества измененных файлов