Pārlūkot izejas kodu

add shared trip screen

Viktoriia 1 nedēļu atpakaļ
vecāks
revīzija
0fd1eccd17
23 mainītis faili ar 1428 papildinājumiem un 135 dzēšanām
  1. 6 0
      Route.tsx
  2. 10 0
      assets/icons/events/star.svg
  3. 13 0
      patches/react-native-pell-rich-editor+1.10.0.patch
  4. 22 10
      src/components/Calendars/RangeCalendar/index.tsx
  5. 14 2
      src/modules/api/events/events-api.ts
  6. 9 1
      src/modules/api/events/events-query-keys.tsx
  7. 8 0
      src/modules/api/events/queries/index.ts
  8. 17 0
      src/modules/api/events/queries/use-post-add-shared-trip.tsx
  9. 22 0
      src/modules/api/events/queries/use-post-cancel-shared-trip.tsx
  10. 22 0
      src/modules/api/events/queries/use-post-get-shared-trip-for-editing.tsx
  11. 17 0
      src/modules/api/events/queries/use-post-join-shared-trip.tsx
  12. 26 0
      src/modules/api/events/queries/use-post-shared-trip-add-participant.tsx
  13. 26 0
      src/modules/api/events/queries/use-post-shared-trip-remove-participant.tsx
  14. 17 0
      src/modules/api/events/queries/use-post-unjoin-shared-trip.tsx
  15. 17 0
      src/modules/api/events/queries/use-post-update-shared-trip.tsx
  16. 15 6
      src/screens/InAppScreens/TravelsScreen/AddRegionsScreen/index.tsx
  17. 21 19
      src/screens/InAppScreens/TravelsScreen/CreateEvent/index.tsx
  18. 77 1
      src/screens/InAppScreens/TravelsScreen/CreateEvent/styles.tsx
  19. 634 0
      src/screens/InAppScreens/TravelsScreen/CreateSharedTrip/index.tsx
  20. 372 91
      src/screens/InAppScreens/TravelsScreen/EventsScreen/index.tsx
  21. 57 2
      src/screens/InAppScreens/TravelsScreen/EventsScreen/styles.tsx
  22. 4 2
      src/types/api.ts
  23. 2 1
      src/types/navigation.ts

+ 6 - 0
Route.tsx

@@ -109,6 +109,7 @@ import EventsNotificationsScreen from 'src/screens/NotificationsScreen/EventsNot
 import SelectOwnMapScreen from 'src/screens/OfflineMapsScreen/SelectOwnMapScreen';
 import { SelectRegionScreen } from 'src/screens/OfflineMapsScreen/SelectRegionsScreen';
 import EditCountryDataScreen from 'src/screens/InAppScreens/TravelsScreen/EditCountryDataScreen';
+import CreateSharedTripScreen from 'src/screens/InAppScreens/TravelsScreen/CreateSharedTrip';
 
 enableScreens();
 
@@ -457,6 +458,11 @@ const Route = () => {
               name={NAVIGATION_PAGES.CREATE_EVENT}
               component={CreateEventScreen}
             />
+            <ScreenStack.Screen
+              name={NAVIGATION_PAGES.CREATE_SHARED_TRIP}
+              component={CreateSharedTripScreen}
+            />
+            <ScreenStack.Screen name={NAVIGATION_PAGES.ADD_REGIONS} component={AddRegionsScreen} />
             <ScreenStack.Screen
               name={NAVIGATION_PAGES.ALL_EVENT_PHOTOS}
               component={AllEventPhotosScreen}

+ 10 - 0
assets/icons/events/star.svg

@@ -0,0 +1,10 @@
+<svg width="15" height="14" viewBox="0 0 15 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_5675_46207)">
+<path d="M8.0857 0.348592C7.97392 0.135711 7.74764 0 7.50226 0C7.25689 0 7.03061 0.135711 6.91883 0.348592L4.91224 4.18576L0.552814 4.86166C0.31017 4.89891 0.10842 5.06656 0.0320828 5.2954C-0.0442548 5.52425 0.0184511 5.77438 0.190211 5.94469L3.30915 8.99154L2.62211 13.2465C2.58394 13.4833 2.68482 13.7228 2.88384 13.8638C3.08286 14.0049 3.34459 14.0262 3.56542 13.9171L7.50226 11.9639L11.4364 13.9171C11.6545 14.0262 11.9189 14.0049 12.118 13.8638C12.317 13.7228 12.4179 13.486 12.3797 13.2465L11.6899 8.99154L14.8089 5.94469C14.9834 5.77438 15.0433 5.52425 14.967 5.2954C14.8907 5.06656 14.6916 4.89891 14.4463 4.86166L10.0896 4.18576L8.0857 0.348592Z" fill="white"/>
+</g>
+<defs>
+<clipPath id="clip0_5675_46207">
+<rect width="15" height="14" fill="white"/>
+</clipPath>
+</defs>
+</svg>

+ 13 - 0
patches/react-native-pell-rich-editor+1.10.0.patch

@@ -0,0 +1,13 @@
+diff --git a/node_modules/react-native-pell-rich-editor/src/RichEditor.js b/node_modules/react-native-pell-rich-editor/src/RichEditor.js
+index f194ac3..1248da3 100644
+--- a/node_modules/react-native-pell-rich-editor/src/RichEditor.js
++++ b/node_modules/react-native-pell-rich-editor/src/RichEditor.js
+@@ -281,7 +281,7 @@ export default class RichTextEditor extends Component {
+           ref={that.setRef}
+           onMessage={that.onMessage}
+           originWhitelist={['*']}
+-          dataDetectorTypes={dataDetectorTypes}
++          // dataDetectorTypes={dataDetectorTypes}
+           domStorageEnabled={false}
+           bounces={false}
+           javaScriptEnabled={true}

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

@@ -14,6 +14,7 @@ export default function RangeCalendar({
   closeModal,
   allowRangeSelection = true,
   disableFutureDates = false,
+  disablePastDates = false,
   highlightedDates,
   selectedDate
 }: {
@@ -21,6 +22,7 @@ export default function RangeCalendar({
   closeModal: (startDate?: string | null, endDate?: string | null) => void;
   allowRangeSelection?: boolean;
   disableFutureDates?: boolean;
+  disablePastDates?: boolean;
   highlightedDates?: string[];
   selectedDate?: string;
 }) {
@@ -51,14 +53,14 @@ export default function RangeCalendar({
     selectedDayBackgroundColor: Colors.ORANGE,
     'stylesheet.calendar.header': {
       header: styles.calendarHeader
-    },
+    }
   };
 
   const onDayPress = (day: any) => {
     if (!allowRangeSelection) {
       setSelectedStartDate(day.dateString);
       return;
-    } 
+    }
     if (!selectedStartDate || (selectedStartDate && selectedEndDate)) {
       setSelectedStartDate(day.dateString);
       setSelectedEndDate(null);
@@ -82,7 +84,7 @@ export default function RangeCalendar({
         startingDay: true,
         endingDay: true,
         color: Colors.ORANGE,
-        textColor: 'white',
+        textColor: 'white'
       };
     }
     if (disableFutureDates) {
@@ -91,24 +93,34 @@ export default function RangeCalendar({
       while (today.isBefore(lastDay, 'day')) {
         const dateString = today.format('YYYY-MM-DD');
         if (!marked[dateString]) {
-          marked[dateString] = {  disableTouchEvent: true, disabled: true };
+          marked[dateString] = { disableTouchEvent: true, disabled: true };
         }
         today.add(1, 'day');
       }
     }
+    if (disablePastDates) {
+      const today = moment().subtract(1, 'day');
+      const firstDay = moment().subtract(2, 'years');
+      while (today.isAfter(firstDay, 'day')) {
+        const dateString = today.format('YYYY-MM-DD');
+        if (!marked[dateString]) {
+          marked[dateString] = { disableTouchEvent: true, disabled: true };
+        }
+        today.subtract(1, 'day');
+      }
+    }
     if (highlightedDates) {
       const datesSet = new Set(highlightedDates);
 
       const startDateMoment = moment(highlightedDates[0]);
       const endDateMoment = moment(highlightedDates[highlightedDates.length - 1]);
-    
+
       for (let m = moment(startDateMoment); m.diff(endDateMoment, 'days') <= 0; m.add(1, 'days')) {
         const dateString = m.format('YYYY-MM-DD');
         if (!datesSet.has(dateString)) {
           marked[dateString] = { disableTouchEvent: true, disabled: true };
         }
       }
-
     }
     if (start && end) {
       marked[start] = { startingDay: true, color: Colors.ORANGE, textColor: 'white' };
@@ -127,7 +139,7 @@ export default function RangeCalendar({
         startingDay: true,
         endingDay: true,
         color: Colors.ORANGE,
-        textColor: 'white',
+        textColor: 'white'
       };
     }
     return marked;
@@ -150,7 +162,7 @@ export default function RangeCalendar({
       visibleInPercent={'auto'}
       visible={isModalVisible}
       onRequestClose={resetSelections}
-      headerTitle={allowRangeSelection ? "Select Dates" : "Select Date" }
+      headerTitle={allowRangeSelection ? 'Select Dates' : 'Select Date'}
     >
       <View style={styles.modalContent}>
         <Calendar
@@ -160,7 +172,7 @@ export default function RangeCalendar({
           enableSwipeMonths={true}
           firstDay={1}
           theme={{
-            ...customThemeStyles,
+            ...customThemeStyles
           }}
           minDate={highlightedDates ? highlightedDates[0] : undefined}
           maxDate={highlightedDates ? highlightedDates[highlightedDates.length - 1] : undefined}
@@ -174,7 +186,7 @@ export default function RangeCalendar({
           disabled={!selectedStartDate}
           variant={!selectedStartDate ? ButtonVariants.OPACITY : ButtonVariants.FILL}
           containerStyles={{ borderWidth: 0 }}
-         />
+        />
       </View>
     </Modal>
   );

+ 14 - 2
src/modules/api/events/events-api.ts

@@ -6,6 +6,8 @@ import { User } from '@api/regions';
 
 export interface PostGetEventsListReturn extends ResponseType {
   data: SingleEvent[];
+  nm: SingleEvent[];
+  community: SingleEvent[];
 }
 
 export type SingleEvent = {
@@ -22,7 +24,7 @@ export type SingleEvent = {
   capacity?: number;
   participants?: number;
   registrations_info: 1 | 2 | 3 | 4 | 5;
-  type: 1 | 2 | 3;
+  type: 1 | 2 | 3 | 4;
   photo: 0 | 1;
   available?: number;
   joined: 0 | 1;
@@ -33,6 +35,9 @@ export type SingleEvent = {
   time_to?: string;
   flag: string | null;
   country: string | null;
+  nm_event: 0 | 1;
+  star: 0 | 1;
+  date_tentative: 0 | 1;
 };
 
 export type Participants = {
@@ -195,6 +200,11 @@ export interface PostAddEvent {
   event: any;
 }
 
+export interface PostAddSharedTrip {
+  token: string;
+  trip: any;
+}
+
 export interface PostUpdateEvent {
   token: string;
   event_id: number;
@@ -249,5 +259,7 @@ export const eventsApi = {
   cancelEvent: (token: string, event_id: number) =>
     request.postForm<ResponseType>(API.CANCEL_EVENT, { token, event_id }),
   removeParticipant: (token: string, event_id: number, user_id: number) =>
-    request.postForm<ResponseType>(API.REMOVE_PARTICIPANT, { token, event_id, user_id })
+    request.postForm<ResponseType>(API.REMOVE_PARTICIPANT, { token, event_id, user_id }),
+  addSharedTrip: (data: PostAddSharedTrip) =>
+    request.postForm<ResponseType>(API.ADD_SHARED_TRIP, data)
 };

+ 9 - 1
src/modules/api/events/events-query-keys.tsx

@@ -16,5 +16,13 @@ export const eventsQueryKeys = {
   getEventForEditing: () => ['getEventForEditing'] as const,
   updateEvent: () => ['updateEvent'] as const,
   cancelEvent: () => ['cancelEvent'] as const,
-  removeParticipant: () => ['removeParticipant'] as const
+  removeParticipant: () => ['removeParticipant'] as const,
+  addSharedTrip: () => ['addSharedTrip'] as const,
+  joinSharedTrip: () => ['joinSharedTrip'] as const,
+  unjoinSharedTrip: () => ['unjoinSharedTrip'] as const,
+  sharedTripAddParticipant: () => ['sharedTripAddParticipant'] as const,
+  sharedTripRemoveParticipant: () => ['sharedTripRemoveParticipant'] as const,
+  getSharedTripForEditing: () => ['getSharedTripForEditing'] as const,
+  updateSharedTrip: () => ['updateSharedTrip'] as const,
+  cancelSharedTrip: () => ['cancelSharedTrip'] as const
 };

+ 8 - 0
src/modules/api/events/queries/index.ts

@@ -14,3 +14,11 @@ export * from './use-post-get-event-for-editing';
 export * from './use-post-update-event';
 export * from './use-post-cancel-event';
 export * from './use-post-remove-participant';
+export * from './use-post-join-shared-trip';
+export * from './use-post-unjoin-shared-trip';
+export * from './use-post-shared-trip-add-participant';
+export * from './use-post-shared-trip-remove-participant';
+export * from './use-post-get-shared-trip-for-editing';
+export * from './use-post-update-shared-trip';
+export * from './use-post-cancel-shared-trip';
+export * from './use-post-add-shared-trip';

+ 17 - 0
src/modules/api/events/queries/use-post-add-shared-trip.tsx

@@ -0,0 +1,17 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { eventsQueryKeys } from '../events-query-keys';
+import { type PostAddSharedTrip, eventsApi } from '../events-api';
+
+import type { BaseAxiosError } from '../../../../types';
+import { ResponseType } from '@api/response-type';
+
+export const usePostAddSharedTripMutation = () => {
+  return useMutation<ResponseType, BaseAxiosError, PostAddSharedTrip, ResponseType>({
+    mutationKey: eventsQueryKeys.addSharedTrip(),
+    mutationFn: async (data) => {
+      const response = await eventsApi.addSharedTrip(data);
+      return response.data;
+    }
+  });
+};

+ 22 - 0
src/modules/api/events/queries/use-post-cancel-shared-trip.tsx

@@ -0,0 +1,22 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { eventsQueryKeys } from '../events-query-keys';
+import { eventsApi } from '../events-api';
+
+import type { BaseAxiosError } from '../../../../types';
+import { ResponseType } from '@api/response-type';
+
+export const usePostCancelSharedTripMutation = () => {
+  return useMutation<
+    ResponseType,
+    BaseAxiosError,
+    { token: string; event_id: number },
+    ResponseType
+  >({
+    mutationKey: eventsQueryKeys.cancelSharedTrip(),
+    mutationFn: async (data) => {
+      const response = await eventsApi.cancelSharedTrip(data.token, data.event_id);
+      return response.data;
+    }
+  });
+};

+ 22 - 0
src/modules/api/events/queries/use-post-get-shared-trip-for-editing.tsx

@@ -0,0 +1,22 @@
+import { useQuery } from '@tanstack/react-query';
+import { useMutation } from '@tanstack/react-query';
+
+import { eventsQueryKeys } from '../events-query-keys';
+import { eventsApi, type PostGetEventForEditingReturn } from '../events-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const useGetSharedTripForEditingMutation = () => {
+  return useMutation<
+    PostGetEventForEditingReturn,
+    BaseAxiosError,
+    { token: string; event_id: number },
+    PostGetEventForEditingReturn
+  >({
+    mutationKey: eventsQueryKeys.getSharedTripForEditing(),
+    mutationFn: async (data) => {
+      const response = await eventsApi.getSharedTripForEditing(data.token, data.event_id);
+      return response.data;
+    }
+  });
+};

+ 17 - 0
src/modules/api/events/queries/use-post-join-shared-trip.tsx

@@ -0,0 +1,17 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { eventsQueryKeys } from '../events-query-keys';
+import { type PostJoinEvent, eventsApi } from '../events-api';
+
+import type { BaseAxiosError } from '../../../../types';
+import { ResponseType } from '@api/response-type';
+
+export const usePostJoinSharedTripMutation = () => {
+  return useMutation<ResponseType, BaseAxiosError, PostJoinEvent, ResponseType>({
+    mutationKey: eventsQueryKeys.joinSharedTrip(),
+    mutationFn: async (data) => {
+      const response = await eventsApi.joinSharedTrip(data);
+      return response.data;
+    }
+  });
+};

+ 26 - 0
src/modules/api/events/queries/use-post-shared-trip-add-participant.tsx

@@ -0,0 +1,26 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { eventsQueryKeys } from '../events-query-keys';
+import { type PostJoinEvent, eventsApi } from '../events-api';
+
+import type { BaseAxiosError } from '../../../../types';
+import { ResponseType } from '@api/response-type';
+
+export const usePostSharedTripAddParticipantMutation = () => {
+  return useMutation<
+    ResponseType,
+    BaseAxiosError,
+    { token: string; event_id: number; user_id: number },
+    ResponseType
+  >({
+    mutationKey: eventsQueryKeys.sharedTripAddParticipant(),
+    mutationFn: async (variables) => {
+      const response = await eventsApi.sharedTripAddParticipant(
+        variables.token,
+        variables.event_id,
+        variables.user_id
+      );
+      return response.data;
+    }
+  });
+};

+ 26 - 0
src/modules/api/events/queries/use-post-shared-trip-remove-participant.tsx

@@ -0,0 +1,26 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { eventsQueryKeys } from '../events-query-keys';
+import { type PostJoinEvent, eventsApi } from '../events-api';
+
+import type { BaseAxiosError } from '../../../../types';
+import { ResponseType } from '@api/response-type';
+
+export const usePostSharedTripRemoveParticipantMutation = () => {
+  return useMutation<
+    ResponseType,
+    BaseAxiosError,
+    { token: string; event_id: number; user_id: number },
+    ResponseType
+  >({
+    mutationKey: eventsQueryKeys.sharedTripRemoveParticipant(),
+    mutationFn: async (variables) => {
+      const response = await eventsApi.sharedTripRemoveParticipant(
+        variables.token,
+        variables.event_id,
+        variables.user_id
+      );
+      return response.data;
+    }
+  });
+};

+ 17 - 0
src/modules/api/events/queries/use-post-unjoin-shared-trip.tsx

@@ -0,0 +1,17 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { eventsQueryKeys } from '../events-query-keys';
+import { type PostJoinEvent, eventsApi } from '../events-api';
+
+import type { BaseAxiosError } from '../../../../types';
+import { ResponseType } from '@api/response-type';
+
+export const usePostUnjoinSharedTripMutation = () => {
+  return useMutation<ResponseType, BaseAxiosError, PostJoinEvent, ResponseType>({
+    mutationKey: eventsQueryKeys.unjoinSharedTrip(),
+    mutationFn: async (data) => {
+      const response = await eventsApi.unjoinSharedTrip(data);
+      return response.data;
+    }
+  });
+};

+ 17 - 0
src/modules/api/events/queries/use-post-update-shared-trip.tsx

@@ -0,0 +1,17 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { eventsQueryKeys } from '../events-query-keys';
+import { type PostUpdateEvent, eventsApi } from '../events-api';
+
+import type { BaseAxiosError } from '../../../../types';
+import { ResponseType } from '@api/response-type';
+
+export const usePostUpdateSharedTripMutation = () => {
+  return useMutation<ResponseType, BaseAxiosError, PostUpdateEvent, ResponseType>({
+    mutationKey: eventsQueryKeys.updateSharedTrip(),
+    mutationFn: async (data) => {
+      const response = await eventsApi.updateSharedTrip(data);
+      return response.data;
+    }
+  });
+};

+ 15 - 6
src/screens/InAppScreens/TravelsScreen/AddRegionsScreen/index.tsx

@@ -125,12 +125,21 @@ const AddRegionsScreen = ({ route }: { route: any }) => {
   };
 
   const handleSavePress = () => {
-    navigation.navigate(
-      ...([
-        NAVIGATION_PAGES.ADD_TRIP,
-        { regionsToSave: regionsToSave, editTripId: route.params?.editId }
-      ] as never)
-    );
+    if (route.params?.isSharedTrip) {
+      navigation.navigate(
+        ...([
+          NAVIGATION_PAGES.CREATE_SHARED_TRIP,
+          { regionsToSave: regionsToSave, eventId: route.params?.editId }
+        ] as never)
+      );
+    } else {
+      navigation.navigate(
+        ...([
+          NAVIGATION_PAGES.ADD_TRIP,
+          { regionsToSave: regionsToSave, editTripId: route.params?.editId }
+        ] as never)
+      );
+    }
   };
 
   const handleSetRegionData = (regionId: number) => {

+ 21 - 19
src/screens/InAppScreens/TravelsScreen/CreateEvent/index.tsx

@@ -49,7 +49,7 @@ const EventSchema = yup.object({
   date: yup.date().nullable().required(),
   time_from: yup.date().nullable().required(),
   time_to: yup.date().nullable().required(),
-  capacity: yup.number().optional(),
+  // capacity: yup.number().optional(),
   region: yup.number().required(),
   photo: yup.number().nullable().optional(),
   city: yup.string().required(),
@@ -88,7 +88,7 @@ const CreateEventScreen = ({ route }: { route: any }) => {
     date: eventData?.date ?? '',
     time_from: eventData?.time_from ? timeStringToDate(eventData.time_from) : null,
     time_to: eventData?.time_to ? timeStringToDate(eventData.time_to) : null,
-    capacity: eventData?.capacity ?? '',
+    // capacity: eventData?.capacity ?? '',
     region: eventData?.region ?? null,
     photo: null,
     city: eventData?.address1 ?? '',
@@ -188,11 +188,11 @@ const CreateEventScreen = ({ route }: { route: any }) => {
                   newEvent.photo = values.photo;
                 }
 
-                if (values.capacity) {
-                  newEvent.capacity = Number(values.capacity);
-                } else {
-                  newEvent.capacity = 0;
-                }
+                // if (values.capacity) {
+                //   newEvent.capacity = Number(values.capacity);
+                // } else {
+                //   newEvent.capacity = 0;
+                // }
 
                 if (values.time_from) {
                   newEvent.time_from = (values.time_from as Date).toLocaleTimeString([], {
@@ -253,7 +253,7 @@ const CreateEventScreen = ({ route }: { route: any }) => {
                     onChange={props.handleChange('event_name')}
                     value={props.values.event_name}
                     onBlur={props.handleBlur('event_name')}
-                    formikError={props.touched.event_name && props.errors.event_name}
+                    formikError={props.touched.event_name && (props.errors.event_name as string)}
                   />
 
                   <Input
@@ -264,7 +264,7 @@ const CreateEventScreen = ({ route }: { route: any }) => {
                     value={props.values.date}
                     onBlur={props.handleBlur('date')}
                     isFocused={() => setCalendarVisible(true)}
-                    formikError={props.touched.date && props.errors.date}
+                    formikError={props.touched.date && (props.errors.date as string)}
                     icon={<CalendarSvg fill={Colors.LIGHT_GRAY} width={20} height={20} />}
                   />
 
@@ -273,7 +273,7 @@ const CreateEventScreen = ({ route }: { route: any }) => {
                       headerTitle={'Select Start Time'}
                       defaultTime={props.values.time_from}
                       selectedTime={(time) => props.setFieldValue('time_from', time)}
-                      formikError={props.touched.time_from && props.errors.time_from}
+                      formikError={props.touched.time_from && (props.errors.time_from as string)}
                       inputHeader={'Start time'}
                       placeholder="Choose a time"
                     />
@@ -282,22 +282,22 @@ const CreateEventScreen = ({ route }: { route: any }) => {
                       headerTitle={'Select End Time'}
                       defaultTime={props.values.time_to}
                       selectedTime={(time) => props.setFieldValue('time_to', time)}
-                      formikError={props.touched.time_to && props.errors.time_to}
+                      formikError={props.touched.time_to && (props.errors.time_to as string)}
                       inputHeader={'End time'}
                       placeholder="Choose a time"
                     />
                   </View>
 
-                  <Input
+                  {/* <Input
                     header={'Maximum capacity'}
                     placeholder={'Set the maximum of people'}
                     inputMode={'numeric'}
                     onChange={props.handleChange('capacity')}
                     value={props.values.capacity}
                     onBlur={props.handleBlur('capacity')}
-                    formikError={props.touched.capacity && props.errors.capacity}
+                    formikError={props.touched.capacity && props.errors.capacity as string}
                     icon={<NomadsIcon fill={Colors.LIGHT_GRAY} width={20} height={20} />}
-                  />
+                  /> */}
 
                   {eventId ? (
                     <Input
@@ -337,7 +337,7 @@ const CreateEventScreen = ({ route }: { route: any }) => {
                           } as any
                         })
                       }
-                      formikError={props.touched.photo && props.errors.photo}
+                      formikError={props.touched.photo && (props.errors.photo as string)}
                       icon={<AddImgSvg fill={Colors.LIGHT_GRAY} width={20} height={20} />}
                     />
                   ) : null}
@@ -370,7 +370,7 @@ const CreateEventScreen = ({ route }: { route: any }) => {
                     onChange={props.handleChange('city')}
                     value={props.values.city}
                     onBlur={props.handleBlur('city')}
-                    formikError={props.touched.city && props.errors.city}
+                    formikError={props.touched.city && (props.errors.city as string)}
                     icon={<EarthIcon fill={Colors.LIGHT_GRAY} width={20} height={20} />}
                   />
 
@@ -381,7 +381,7 @@ const CreateEventScreen = ({ route }: { route: any }) => {
                     onChange={props.handleChange('address')}
                     value={props.values.address}
                     onBlur={props.handleBlur('address')}
-                    formikError={props.touched.address && props.errors.address}
+                    formikError={props.touched.address && (props.errors.address as string)}
                     icon={<LocationIcon fill={Colors.LIGHT_GRAY} width={20} height={20} />}
                   />
 
@@ -403,7 +403,9 @@ const CreateEventScreen = ({ route }: { route: any }) => {
                         } as any
                       })
                     }
-                    formikError={props.touched.pin && !props.values.pin ? props.errors.pin : false}
+                    formikError={
+                      props.touched.pin && !props.values.pin ? (props.errors.pin as string) : false
+                    }
                     icon={<LocationIcon fill={Colors.LIGHT_GRAY} width={20} height={20} />}
                   />
 
@@ -503,7 +505,7 @@ const CreateEventScreen = ({ route }: { route: any }) => {
                           marginTop: 5
                         }}
                       >
-                        {props.errors.details}
+                        {props.errors.details as string}
                       </Text>
                     ) : null}
                   </View>

+ 77 - 1
src/screens/InAppScreens/TravelsScreen/CreateEvent/styles.tsx

@@ -2,4 +2,80 @@ import { Dimensions, StyleSheet } from 'react-native';
 import { Colors } from '../../../../theme';
 import { getFontSize } from 'src/utils';
 
-export const styles = StyleSheet.create({});
+export const styles = StyleSheet.create({
+  addRegionBtn: {
+    display: 'flex',
+    justifyContent: 'center',
+    alignItems: 'center',
+    borderRadius: 4,
+    gap: 10,
+    padding: 10,
+    borderColor: Colors.DARK_BLUE,
+    borderWidth: 1,
+    borderStyle: 'solid',
+    marginBottom: 12
+  },
+  addRegionBtntext: {
+    color: Colors.DARK_BLUE,
+    fontSize: getFontSize(14),
+    fontFamily: 'redhat-700'
+  },
+  regionItem: {
+    paddingVertical: 8,
+    paddingHorizontal: 12,
+    gap: 4,
+    borderRadius: 8,
+    backgroundColor: Colors.FILL_LIGHT,
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'space-between'
+  },
+  regionHeader: { flexDirection: 'row', alignItems: 'center', flex: 1 },
+  flagStyle: {
+    width: 32,
+    height: 32,
+    borderRadius: 16,
+    borderWidth: 1,
+    borderColor: Colors.LIGHT_GRAY,
+    marginRight: 12
+  },
+  nameContainer: {
+    flexShrink: 1
+  },
+  regionName: {
+    fontSize: 14,
+    fontWeight: 'bold',
+    color: Colors.DARK_BLUE
+  },
+  regionSubname: {
+    fontSize: 12,
+    fontWeight: '400',
+    color: Colors.DARK_BLUE
+  },
+  trashBtn: {
+    backgroundColor: Colors.RED,
+    padding: 8,
+    borderRadius: 50
+  },
+  regionsContainer: {
+    gap: 12
+  },
+  noRegiosText: {
+    fontSize: 14,
+    fontWeight: '500',
+    color: Colors.LIGHT_GRAY,
+    textAlign: 'center',
+    paddingVertical: 8
+  },
+  optionBtn: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    gap: 8,
+    justifyContent: 'space-between'
+  },
+  optionText: {
+    color: Colors.DARK_BLUE,
+    fontSize: getFontSize(14),
+    fontFamily: 'redhat-700'
+  }
+});

+ 634 - 0
src/screens/InAppScreens/TravelsScreen/CreateSharedTrip/index.tsx

@@ -0,0 +1,634 @@
+import React, { useCallback, useEffect, useRef, useState } from 'react';
+import {
+  View,
+  Text,
+  Image,
+  TouchableOpacity,
+  ScrollView,
+  KeyboardAvoidingView,
+  Platform,
+  TouchableWithoutFeedback,
+  Keyboard
+} from 'react-native';
+import { Formik } from 'formik';
+import * as yup from 'yup';
+import { useNavigation } from '@react-navigation/native';
+import { styles } from '../CreateEvent/styles';
+import { ButtonVariants } from 'src/types/components';
+import ImageView from 'better-react-native-image-viewing';
+
+import { StoreType, storage } from 'src/storage';
+
+import AddImgSvg from 'assets/icons/travels-screens/add-img.svg';
+import { Button, CheckBox, Header, Input, PageWrapper, WarningModal } from 'src/components';
+import { Colors } from 'src/theme';
+
+import { getFontSize } from 'src/utils';
+import { RichEditor, RichToolbar, actions } from 'react-native-pell-rich-editor';
+import CalendarSvg from '../../../../../assets/icons/calendar.svg';
+import RangeCalendar from 'src/components/Calendars/RangeCalendar';
+import {
+  usePostAddSharedTripMutation,
+  usePostCancelEventMutation,
+  usePostGetPhotosForRegionMutation,
+  usePostUpdateEventMutation
+} from '@api/events';
+import { SheetManager } from 'react-native-actions-sheet';
+import PhotosForRegionModal from '../Components/PhotosForRegionModal/PhotosForRegionModal';
+import { API_HOST } from 'src/constants';
+import AddMapPinModal from '../Components/AddMapPinModal';
+import { NAVIGATION_PAGES } from 'src/types';
+import TrashSvg from 'assets/icons/travels-screens/trash-solid.svg';
+
+const EventSchema = yup.object({
+  title: yup.string().required().min(3),
+  start_date: yup.date().nullable().required(),
+  end_date: yup.date().nullable().required(),
+  tentative: yup.number().optional(),
+  photo: yup.number().nullable().optional(),
+  details: yup.string().required()
+});
+
+const CreateSharedTripScreen = ({ route }: { route: any }) => {
+  const eventId = route.params?.eventId;
+  // TO DO
+  const eventData = route.params?.event;
+  const token = (storage.get('token', StoreType.STRING) as string) ?? null;
+  const navigation = useNavigation();
+  const scrollRef = useRef<ScrollView>(null);
+  const richText = useRef<RichEditor | null>(null);
+  const { mutateAsync: getPhotosForRegion } = usePostGetPhotosForRegionMutation();
+  const { mutateAsync: addSharedTrip } = usePostAddSharedTripMutation();
+  const { mutateAsync: updateEvent } = usePostUpdateEventMutation();
+  const { mutateAsync: cancelEvent } = usePostCancelEventMutation();
+
+  const [isSubmitting, setIsSubmitting] = useState(false);
+  const [calendarVisible, setCalendarVisible] = useState<'start_date' | 'end_date' | null>(null);
+  const [isViewerVisible, setIsViewerVisible] = useState(false);
+  const [photos, setPhotos] = useState<any[]>([]);
+  const [regions, setRegions] = useState<any[]>(route.params?.regionsToSave ?? []);
+  const [regionsError, setRegionsError] = useState<string | null>(null);
+
+  useEffect(() => {
+    if (route.params?.regionsToSave) {
+      setRegions(route.params.regionsToSave);
+    }
+  }, [route.params?.regionsToSave]);
+
+  useEffect(() => {
+    if (regions && regions.length > 0) {
+      handleGetPhotosForAllRegions(regions);
+    } else {
+      setPhotos([]);
+    }
+  }, [regions]);
+
+  const handleGetPhotosForAllRegions = useCallback(
+    async (regionsArray: any[]) => {
+      if (!regionsArray || regionsArray.length === 0) {
+        setPhotos([]);
+        return;
+      }
+
+      const allPhotos: any[] = [];
+
+      try {
+        for (const region of regionsArray) {
+          await getPhotosForRegion(
+            { region_id: region.id },
+            {
+              onSuccess: (res) => {
+                if (res.photos && res.photos.length > 0) {
+                  allPhotos.push(...res.photos);
+                }
+              },
+              onError: (error) => {
+                console.log(`Error loading photos for region ${region.id}:`, error);
+              }
+            }
+          );
+        }
+
+        setPhotos(allPhotos);
+      } catch (error) {
+        setPhotos([]);
+      }
+    },
+    [getPhotosForRegion, token]
+  );
+
+  const initialData: any = {
+    title: eventData?.title ?? '',
+    start_date: eventData?.start_date ?? '',
+    end_date: eventData?.end_date ?? '',
+    tentative: eventData?.tentative ?? 0,
+    photo: null,
+    details: eventData?.details ?? ''
+  };
+
+  const [modalInfo, setModalInfo] = useState({
+    visible: false,
+    type: 'success',
+    title: '',
+    message: '',
+    buttonTitle: 'OK',
+    action: () => {}
+  });
+
+  useEffect(() => {
+    if (eventData && eventData.regions) {
+      setRegions(eventData.regions);
+    }
+  }, [eventData]);
+
+  const handleCancelTrip = () => {
+    setModalInfo({
+      visible: true,
+      type: 'delete',
+      title: 'Cancel trip',
+      buttonTitle: 'Cancel',
+      message: `Are you sure you want to cancel this trip?`,
+      action: () => {
+        cancelEvent(
+          { token, event_id: eventId },
+          {
+            onSuccess: (res) => {
+              navigation.navigate(NAVIGATION_PAGES.EVENTS as never);
+            }
+          }
+        );
+      }
+    });
+  };
+
+  const handleDeleteRegion = (regionId: number) => {
+    regions && setRegions(regions.filter((region) => region.id !== regionId));
+  };
+
+  return (
+    <PageWrapper>
+      <Header label="Add trip" />
+
+      <KeyboardAvoidingView
+        behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
+        style={{ flex: 1 }}
+      >
+        <TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
+          <ScrollView ref={scrollRef} showsVerticalScrollIndicator={false}>
+            <Formik
+              validationSchema={EventSchema}
+              initialValues={initialData}
+              onSubmit={async (values) => {
+                if (regions.length === 0) {
+                  return;
+                }
+                setIsSubmitting(true);
+
+                const regionsToSave = regions.map((region) => region.id);
+
+                const newTrip: any = {
+                  title: values.title,
+                  start_date: values.start_date,
+                  end_date: values.end_date,
+                  tentative: values.tentative,
+                  regions: regionsToSave,
+                  details: values.details
+                };
+
+                if (values.photo) {
+                  newTrip.photo = values.photo;
+                }
+
+                if (eventId) {
+                  await updateEvent(
+                    { token, event_id: eventId, event: JSON.stringify(newTrip) },
+                    {
+                      onSuccess: (res) => {
+                        setIsSubmitting(false);
+                        navigation.goBack();
+                      },
+                      onError: (err) => {
+                        setIsSubmitting(false);
+                      }
+                    }
+                  );
+                } else {
+                  console.log('newTrip', newTrip);
+                  await addSharedTrip(
+                    { token, trip: JSON.stringify(newTrip) },
+                    {
+                      onSuccess: (res) => {
+                        setIsSubmitting(false);
+                        console.log('res', res);
+                        navigation.goBack();
+                      },
+                      onError: (err) => {
+                        setIsSubmitting(false);
+                        console.log('err', err);
+                      }
+                    }
+                  );
+                }
+              }}
+            >
+              {(props) => (
+                <View style={{ gap: 12 }}>
+                  <Input
+                    header={'Trip name'}
+                    placeholder={'Add trip name'}
+                    inputMode={'text'}
+                    onChange={props.handleChange('title')}
+                    value={props.values.title}
+                    onBlur={props.handleBlur('title')}
+                    formikError={props.touched.title && (props.errors.title as string)}
+                  />
+
+                  <View style={{ flexDirection: 'row', gap: 8 }}>
+                    <View style={{ flex: 1 }}>
+                      <Input
+                        header={'From'}
+                        placeholder={'Add start date'}
+                        inputMode={'none'}
+                        onChange={props.handleChange('start_date')}
+                        value={props.values.start_date}
+                        onBlur={props.handleBlur('start_date')}
+                        isFocused={() => setCalendarVisible('start_date')}
+                        formikError={
+                          props.touched.start_date && (props.errors.start_date as string)
+                        }
+                        icon={<CalendarSvg fill={Colors.LIGHT_GRAY} width={20} height={20} />}
+                      />
+                    </View>
+
+                    <View style={{ flex: 1 }}>
+                      <Input
+                        header={'To'}
+                        placeholder={'Add end date'}
+                        inputMode={'none'}
+                        onChange={props.handleChange('end_date')}
+                        value={props.values.end_date}
+                        onBlur={props.handleBlur('end_date')}
+                        isFocused={() => setCalendarVisible('end_date')}
+                        formikError={props.touched.end_date && (props.errors.end_date as string)}
+                        icon={<CalendarSvg fill={Colors.LIGHT_GRAY} width={20} height={20} />}
+                      />
+                    </View>
+                  </View>
+
+                  <TouchableOpacity
+                    onPress={() => {
+                      props.setFieldValue('tentative', props.values.tentative === 1 ? 0 : 1);
+                    }}
+                    style={styles.optionBtn}
+                    hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
+                  >
+                    <Text style={styles.optionText}>Tentative dates</Text>
+                    <CheckBox
+                      onChange={() => {
+                        props.setFieldValue('tentative', props.values.tentative === 1 ? 0 : 1);
+                      }}
+                      value={props.values.tentative === 1}
+                      color={Colors.DARK_BLUE}
+                    />
+                  </TouchableOpacity>
+
+                  {eventId ? (
+                    <Input
+                      header={'NomadMania region'}
+                      inputMode={'none'}
+                      editable={false}
+                      value={eventData?.region_name}
+                    />
+                  ) : (
+                    <View>
+                      <Text
+                        style={{
+                          color: Colors.DARK_BLUE,
+                          fontSize: getFontSize(14),
+                          fontFamily: 'redhat-700',
+                          marginBottom: 8
+                        }}
+                      >
+                        NomadMania regions
+                      </Text>
+
+                      <TouchableOpacity
+                        style={styles.addRegionBtn}
+                        onPress={() =>
+                          navigation.navigate(
+                            ...([
+                              NAVIGATION_PAGES.ADD_REGIONS,
+                              { regionsParams: regions, editId: eventId, isSharedTrip: true }
+                            ] as never)
+                          )
+                        }
+                      >
+                        <Text style={styles.addRegionBtntext}>Add Region</Text>
+                      </TouchableOpacity>
+
+                      {regions && regions.length ? (
+                        <View style={styles.regionsContainer}>
+                          {regions.map((region) => {
+                            const [name, ...rest] = region.region_name?.split(/ – | - /);
+                            const subname = rest?.join(' - ');
+
+                            return (
+                              <View key={region.id} style={styles.regionItem}>
+                                <View style={styles.regionHeader}>
+                                  <Image
+                                    source={{ uri: `${API_HOST}/img/flags_new/${region.flag1}` }}
+                                    style={styles.flagStyle}
+                                  />
+                                  {region.flag2 && (
+                                    <Image
+                                      source={{
+                                        uri: `${API_HOST}/img/flags_new/${region.flag2}`
+                                      }}
+                                      style={[styles.flagStyle, { marginLeft: -17 }]}
+                                    />
+                                  )}
+                                  <View style={styles.nameContainer}>
+                                    <Text style={styles.regionName}>{name}</Text>
+                                    <Text style={styles.regionSubname}>{subname}</Text>
+                                  </View>
+                                </View>
+                                <TouchableOpacity
+                                  style={styles.trashBtn}
+                                  onPress={() => handleDeleteRegion(region.id)}
+                                >
+                                  <TrashSvg fill={Colors.WHITE} />
+                                </TouchableOpacity>
+                              </View>
+                            );
+                          })}
+                        </View>
+                      ) : regionsError ? (
+                        <Text
+                          style={{
+                            color: Colors.RED,
+                            fontSize: getFontSize(12),
+                            fontFamily: 'redhat-600',
+                            marginTop: 5
+                          }}
+                        >
+                          {regionsError}
+                        </Text>
+                      ) : (
+                        <Text style={styles.noRegiosText}>No regions at the moment</Text>
+                      )}
+                    </View>
+                  )}
+
+                  {regions && regions.length > 0 && photos.length > 0 ? (
+                    <Input
+                      header={'Photo'}
+                      placeholder={props.values.photo ? 'Photo selected' : 'Choose a photo'}
+                      inputMode={'none'}
+                      onBlur={props.handleBlur('photo')}
+                      isFocused={() =>
+                        SheetManager.show('photos-for-region-modal', {
+                          payload: {
+                            photos,
+                            selectPhoto: (photo: any) => {
+                              props.setFieldValue('photo', photo);
+                            }
+                          } as any
+                        })
+                      }
+                      formikError={props.touched.photo && (props.errors.photo as string)}
+                      icon={<AddImgSvg fill={Colors.LIGHT_GRAY} width={20} height={20} />}
+                    />
+                  ) : null}
+
+                  {props.values.photo ? (
+                    <View
+                      style={{
+                        display: 'flex',
+                        justifyContent: 'center',
+                        alignItems: 'center',
+                        gap: 8
+                      }}
+                    >
+                      <TouchableOpacity
+                        style={{ width: '100%' }}
+                        onPress={async () => setIsViewerVisible(true)}
+                      >
+                        <Image
+                          source={{ uri: `${API_HOST}/webapi/photos/${props.values.photo}/small` }}
+                          style={{ width: '100%', height: 200, borderRadius: 4 }}
+                        />
+                      </TouchableOpacity>
+                    </View>
+                  ) : null}
+
+                  <View>
+                    <Text
+                      style={{
+                        color: Colors.DARK_BLUE,
+                        fontSize: getFontSize(14),
+                        fontFamily: 'redhat-700',
+                        marginBottom: 8
+                      }}
+                    >
+                      Details
+                    </Text>
+                    <RichToolbar
+                      editor={richText}
+                      selectedButtonStyle={{ backgroundColor: Colors.LIGHT_GRAY }}
+                      style={[
+                        {
+                          borderTopLeftRadius: 4,
+                          borderTopRightRadius: 4,
+                          backgroundColor: Colors.FILL_LIGHT
+                        },
+                        props.touched.details && props.errors.details
+                          ? {
+                              borderTopColor: Colors.RED,
+                              borderLeftColor: Colors.RED,
+                              borderRightColor: Colors.RED,
+                              borderWidth: 1,
+                              borderBottomWidth: 0
+                            }
+                          : {}
+                      ]}
+                      actions={[
+                        actions.keyboard,
+                        actions.setBold,
+                        actions.setItalic,
+                        actions.setUnderline,
+                        actions.alignLeft,
+                        actions.alignCenter,
+                        actions.alignRight,
+                        actions.alignFull,
+                        actions.insertLink,
+                        actions.insertBulletsList,
+                        actions.insertOrderedList,
+                        actions.setStrikethrough,
+                        actions.indent,
+                        actions.outdent,
+                        actions.undo,
+                        actions.redo,
+                        actions.blockquote,
+                        actions.insertLine
+                      ]}
+                    />
+
+                    <RichEditor
+                      ref={richText}
+                      initialHeight={100}
+                      onFocus={() => {
+                        scrollRef.current?.scrollTo({
+                          y: 650,
+                          animated: true
+                        });
+                      }}
+                      placeholder="Add trip details"
+                      initialContentHTML={props.values.details}
+                      onChange={props.handleChange('details')}
+                      editorStyle={{
+                        contentCSSText: 'font-size: 14px; padding: 12px; color: #C8C8C8;',
+                        backgroundColor: Colors.FILL_LIGHT,
+                        color: Colors.DARK_BLUE
+                      }}
+                      style={[
+                        {
+                          minHeight: 100,
+                          borderBottomLeftRadius: 4,
+                          borderBottomRightRadius: 4
+                        },
+                        props.touched.details && props.errors.details
+                          ? {
+                              borderBottomColor: Colors.RED,
+                              borderLeftColor: Colors.RED,
+                              borderRightColor: Colors.RED,
+                              borderWidth: 1,
+                              borderTopWidth: 0
+                            }
+                          : {}
+                      ]}
+                      onBlur={() => props.handleBlur('details')}
+                    />
+                    {props.touched.details && props.errors.details ? (
+                      <Text
+                        style={{
+                          color: Colors.RED,
+                          fontSize: getFontSize(12),
+                          fontFamily: 'redhat-600',
+                          marginTop: 5
+                        }}
+                      >
+                        {props.errors.details as string}
+                      </Text>
+                    ) : null}
+                  </View>
+
+                  {eventId ? (
+                    <View style={{ marginTop: 15, marginBottom: 15, gap: 8 }}>
+                      <Button
+                        onPress={() => {
+                          if (regions.length === 0) {
+                            setRegionsError('Please add at least one region.');
+                          } else {
+                            setRegionsError(null);
+                          }
+                          props.handleSubmit();
+                        }}
+                        disabled={isSubmitting}
+                      >
+                        Update trip
+                      </Button>
+
+                      <Button
+                        variant={ButtonVariants.OPACITY}
+                        containerStyles={{
+                          backgroundColor: Colors.RED,
+                          borderColor: Colors.RED
+                        }}
+                        textStyles={{ color: Colors.WHITE }}
+                        onPress={handleCancelTrip}
+                      >
+                        Cancel trip
+                      </Button>
+                    </View>
+                  ) : (
+                    <View style={{ marginTop: 15, marginBottom: 15, gap: 8 }}>
+                      <Button
+                        onPress={() => {
+                          if (regions.length === 0) {
+                            setRegionsError('please add at least one region');
+                          } else {
+                            setRegionsError(null);
+                          }
+                          props.handleSubmit();
+                        }}
+                        disabled={isSubmitting}
+                      >
+                        Save
+                      </Button>
+
+                      <Button
+                        variant={ButtonVariants.OPACITY}
+                        containerStyles={{
+                          backgroundColor: Colors.WHITE,
+                          borderColor: Colors.BORDER_LIGHT
+                        }}
+                        textStyles={{ color: Colors.DARK_BLUE }}
+                        onPress={() => navigation.goBack()}
+                      >
+                        Cancel
+                      </Button>
+                    </View>
+                  )}
+
+                  <RangeCalendar
+                    isModalVisible={calendarVisible ? true : false}
+                    closeModal={(startDate?: string | null, endDate?: string | null) => {
+                      startDate && props.handleChange(calendarVisible)(startDate.toString());
+                      setCalendarVisible(null);
+                    }}
+                    allowRangeSelection={false}
+                    selectedDate={props.values[calendarVisible ?? 'start_date']}
+                    disablePastDates={true}
+                  />
+                  <ImageView
+                    images={[
+                      {
+                        uri: `${API_HOST}/webapi/photos/${props.values.photo}/full`
+                      }
+                    ]}
+                    keyExtractor={(imageSrc, index) => index.toString()}
+                    imageIndex={0}
+                    visible={isViewerVisible}
+                    onRequestClose={() => setIsViewerVisible(false)}
+                    swipeToCloseEnabled={false}
+                    backgroundColor={Colors.DARK_BLUE}
+                  />
+                </View>
+              )}
+            </Formik>
+          </ScrollView>
+        </TouchableWithoutFeedback>
+      </KeyboardAvoidingView>
+      <PhotosForRegionModal />
+      <AddMapPinModal />
+      <WarningModal
+        type={modalInfo.type}
+        isVisible={modalInfo.visible}
+        buttonTitle={modalInfo.buttonTitle}
+        message={modalInfo.message}
+        action={modalInfo.action}
+        onClose={() => {
+          if (modalInfo.type === 'success') {
+            navigation.goBack();
+            setModalInfo({ ...modalInfo, visible: false });
+          } else {
+            setModalInfo({ ...modalInfo, visible: false });
+          }
+        }}
+        title={modalInfo.title}
+      />
+    </PageWrapper>
+  );
+};
+
+export default CreateSharedTripScreen;

+ 372 - 91
src/screens/InAppScreens/TravelsScreen/EventsScreen/index.tsx

@@ -6,11 +6,19 @@ import {
   TouchableOpacity,
   ScrollView,
   LayoutAnimation,
-  findNodeHandle
+  Modal
 } from 'react-native';
 import { FlashList } from '@shopify/flash-list';
 import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
-import { styles } from './styles';
+import { popupStyles, styles } from './styles';
+import Animated, {
+  useSharedValue,
+  useAnimatedStyle,
+  withTiming,
+  runOnJS,
+  interpolate,
+  Extrapolation
+} from 'react-native-reanimated';
 
 import { NAVIGATION_PAGES } from 'src/types';
 import { StoreType, storage } from 'src/storage';
@@ -24,13 +32,40 @@ import EarthIcon from 'assets/icons/travels-section/earth.svg';
 import NomadsIcon from 'assets/icons/bottom-navigation/travellers.svg';
 import CalendarPlusIcon from 'assets/icons/events/calendar-plus.svg';
 import ShoppingCartIcon from 'assets/icons/events/shopping-cart.svg';
-import { SingleEvent, useGetCanAddEventQuery, useGetEventsListQuery } from '@api/events';
+import StarIcon from 'assets/icons/events/star.svg';
+
+import {
+  PostGetEventsListReturn,
+  SingleEvent,
+  useGetCanAddEventQuery,
+  useGetEventsListQuery
+} from '@api/events';
 import moment from 'moment';
 import { API_HOST } from 'src/constants';
 import { Grayscale } from 'react-native-color-matrix-image-filters';
 import { renderSpotsText } from './utils';
 import ChevronIcon from 'assets/icons/chevron-left.svg';
 import Tooltip from 'react-native-walkthrough-tooltip';
+import { TabBar, TabView } from 'react-native-tab-view';
+
+function TabViewDelayed({
+  children,
+  waitBeforeShow = 0
+}: {
+  children: React.ReactNode;
+  waitBeforeShow?: number;
+}) {
+  const [isShown, setIsShown] = useState(false);
+
+  useEffect(() => {
+    const timer = setTimeout(() => {
+      setIsShown(true);
+    }, waitBeforeShow);
+    return () => clearTimeout(timer);
+  }, [waitBeforeShow]);
+
+  return isShown ? children : null;
+}
 
 const EventsScreen = () => {
   const token = (storage.get('token', StoreType.STRING) as string) ?? null;
@@ -39,44 +74,156 @@ const EventsScreen = () => {
   const { data: canAddEvent } = useGetCanAddEventQuery(token, true);
   const navigation = useNavigation();
   const [searchQuery, setSearchQuery] = useState('');
-  const [events, setEvents] = useState<SingleEvent[]>([]);
-  const [pastEvents, setPastEvents] = useState<SingleEvent[]>([]);
-  const [filteredEvents, setFilteredEvents] = useState<SingleEvent[]>([]);
-  const [filteredPastEvents, setFilteredPastEvents] = useState<SingleEvent[]>([]);
+  const [events, setEvents] = useState<PostGetEventsListReturn>({
+    data: [],
+    nm: [],
+    community: []
+  } as never);
+  const [pastEvents, setPastEvents] = useState<PostGetEventsListReturn>({
+    data: [],
+    nm: [],
+    community: []
+  } as never);
+  const [filteredEvents, setFilteredEvents] = useState<PostGetEventsListReturn>({
+    data: [],
+    nm: [],
+    community: []
+  } as never);
+  const [filteredPastEvents, setFilteredPastEvents] = useState<PostGetEventsListReturn>({
+    data: [],
+    nm: [],
+    community: []
+  } as never);
   const [tooltipStates, setTooltipStates] = useState<Record<number, boolean>>({});
   const date = new Date();
 
-  const [isExpanded, setIsExpanded] = useState(false);
-  const scrollViewRef = useRef<ScrollView>(null);
+  const [expandedStates, setExpandedStates] = useState<Record<string, boolean>>({
+    nm: false,
+    community: false,
+    data: false
+  });
+
+  const scrollViewRefs = useRef<Record<string, FlashList<SingleEvent> | null>>({
+    nm: null,
+    community: null,
+    data: null
+  });
   const sectionRef = useRef<View>(null);
 
-  const toggleExpand = () => {
+  const [showPopup, setShowPopup] = useState(false);
+  const [popupPosition, setPopupPosition] = useState({ x: 0, y: 0 });
+
+  const buttonRef = useRef<TouchableOpacity>(null);
+
+  const [index, setIndex] = useState<number>(0);
+  const [routes] = useState<{ key: 'nm' | 'community' | 'data'; title: string }[]>([
+    { key: 'nm', title: 'NomadMania Events' },
+    { key: 'community', title: 'Community Events' },
+    { key: 'data', title: 'All Events' }
+  ]);
+
+  const SEARCH_CONTAINER_HEIGHT = 44;
+  const searchContainerHeight = useSharedValue(SEARCH_CONTAINER_HEIGHT);
+  const lastScrollY = useRef(0);
+  const isSearchVisible = useRef(true);
+
+  const hideSearchContainer = useCallback(() => {
+    'worklet';
+    if (isSearchVisible.current) {
+      isSearchVisible.current = false;
+      searchContainerHeight.value = withTiming(0, {
+        duration: 150
+      });
+    }
+  }, []);
+
+  const showSearchContainer = useCallback(() => {
+    'worklet';
+    if (!isSearchVisible.current) {
+      isSearchVisible.current = true;
+      searchContainerHeight.value = withTiming(SEARCH_CONTAINER_HEIGHT, {
+        duration: 150
+      });
+    }
+  }, []);
+
+  const handleScroll = useCallback(
+    (event: any) => {
+      const currentScrollY = event.nativeEvent.contentOffset.y;
+      const diff = currentScrollY - lastScrollY.current;
+
+      if (diff > 3 && currentScrollY > 20 && isSearchVisible.current) {
+        runOnJS(hideSearchContainer)();
+      } else if (currentScrollY <= 5 && !isSearchVisible.current) {
+        runOnJS(showSearchContainer)();
+      }
+
+      lastScrollY.current = currentScrollY;
+    },
+    [hideSearchContainer, showSearchContainer]
+  );
+
+  const searchContainerAnimatedStyle = useAnimatedStyle(() => {
+    return {
+      height: searchContainerHeight.value,
+      overflow: 'hidden',
+      opacity: interpolate(
+        searchContainerHeight.value,
+        [0, SEARCH_CONTAINER_HEIGHT],
+        [0, 1],
+        Extrapolation.CLAMP
+      )
+    };
+  });
+
+  const handleAddButtonPress = () => {
+    if (buttonRef.current) {
+      buttonRef.current.measure((x, y, width, height, pageX, pageY) => {
+        setPopupPosition({
+          x: pageX - 120,
+          y: pageY + height + 5
+        });
+        setShowPopup(true);
+      });
+    }
+  };
+
+  const handlePopupOption = (option: 'meeting' | 'trip') => {
+    setShowPopup(false);
+
+    if (option === 'meeting') {
+      navigation.navigate(NAVIGATION_PAGES.CREATE_EVENT as never);
+    } else if (option === 'trip') {
+      navigation.navigate(NAVIGATION_PAGES.CREATE_SHARED_TRIP as never);
+    }
+  };
+
+  const toggleExpand = (tabKey: string) => {
     LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut, () => {
-      if (!isExpanded && sectionRef.current && scrollViewRef.current) {
-        sectionRef.current.measureLayout(
-          findNodeHandle(scrollViewRef.current)!,
-          (x, y) => {
-            scrollViewRef.current?.scrollTo({ y, animated: true });
-          },
-          () => console.warn('events measureLayout error')
-        );
+      if (!expandedStates[tabKey] && sectionRef.current && scrollViewRefs.current[tabKey]) {
+        scrollViewRefs.current[tabKey]?.scrollToEnd({
+          animated: true
+        });
       }
     });
 
-    setIsExpanded(!isExpanded);
+    setExpandedStates((prev) => ({
+      ...prev,
+      [tabKey]: !prev[tabKey]
+    }));
   };
 
   useEffect(() => {
     if (data && data.data) {
-      setEvents(data.data);
-      setFilteredEvents(data.data);
+      setEvents(data);
+      setFilteredEvents(data);
     }
   }, [data]);
 
   useEffect(() => {
     if (pastData && pastData.data) {
-      setPastEvents(pastData.data);
-      setFilteredPastEvents(pastData.data);
+      setPastEvents(pastData);
+      setFilteredPastEvents(pastData);
     }
   }, [pastData]);
 
@@ -89,20 +236,36 @@ const EventsScreen = () => {
   const handleSearch = (text: string) => {
     if (text) {
       const searchData =
-        events.filter((item: any) => {
-          const itemData = item.name ? item.name.toLowerCase() : ''.toLowerCase();
-          const textData = text.toLowerCase();
-          return itemData.indexOf(textData) > -1;
-        }) ?? [];
-      setFilteredEvents(searchData);
+        (index === 0 ? events.nm : index === 1 ? events.community : events.data).filter(
+          (item: any) => {
+            const itemData = item.name ? item.name.toLowerCase() : ''.toLowerCase();
+            const textData = text.toLowerCase();
+            return itemData.indexOf(textData) > -1;
+          }
+        ) ?? [];
+      setFilteredEvents(
+        index === 0
+          ? { ...events, nm: searchData }
+          : index === 1
+            ? { ...events, community: searchData }
+            : { ...events, data: searchData }
+      );
 
       const searchPastData =
-        pastEvents.filter((item: any) => {
-          const itemData = item.name ? item.name.toLowerCase() : ''.toLowerCase();
-          const textData = text.toLowerCase();
-          return itemData.indexOf(textData) > -1;
-        }) ?? [];
-      setFilteredPastEvents(searchPastData);
+        (index === 0 ? pastEvents.nm : index === 1 ? pastEvents.community : pastEvents.data).filter(
+          (item: any) => {
+            const itemData = item.name ? item.name.toLowerCase() : ''.toLowerCase();
+            const textData = text.toLowerCase();
+            return itemData.indexOf(textData) > -1;
+          }
+        ) ?? [];
+      setFilteredPastEvents(
+        index === 0
+          ? { ...events, nm: searchPastData }
+          : index === 1
+            ? { ...events, community: searchPastData }
+            : { ...events, data: searchPastData }
+      );
 
       setSearchQuery(text);
     } else {
@@ -114,8 +277,17 @@ const EventsScreen = () => {
 
   const formatEventDate = (event: SingleEvent) => {
     if (event.date_from && event.date_to) {
+      if (event.date_tentative) {
+        return `${moment(event.date_from, 'YYYY-MM').format('MMM YYYY')} - ${moment(
+          event.date_to,
+          'YYYY-MM'
+        ).format('MMM YYYY')}`;
+      }
       return `${moment(event.date_from, 'YYYY-MM-DD').format('DD MMM YYYY')} - ${moment(event.date_to, 'YYYY-MM-DD').format('DD MMM YYYY')}`;
     } else {
+      if (event.date_tentative) {
+        return `${moment(event.date, 'YYYY-MM').format('MMM YYYY')}`;
+      }
       return moment(event.date, 'YYYY-MM-DD').format('DD MMMM YYYY');
     }
   };
@@ -138,7 +310,7 @@ const EventsScreen = () => {
     }
 
     const photo = item.photo
-      ? API_HOST + '/webapi/events/get-square-photo/' + item.id + '?cacheBust=' + date
+      ? API_HOST + '/webapi/events/get-square-photo/' + item.id
       : API_HOST + staticImgUrl;
 
     return (
@@ -156,13 +328,17 @@ const EventsScreen = () => {
           disabled={item.active === 0}
         >
           <View style={styles.imageWrapper}>
-            <Image source={{ uri: photo }} style={styles.image} resizeMode="cover" />
+            <Image
+              source={{ uri: photo, cache: 'reload' }}
+              style={styles.image}
+              resizeMode="cover"
+            />
 
-            {/* {item.isPaid && (
-          <View style={styles.iconOverlay}>
-            <ShoppingCartIcon fill={Colors.WHITE} width={12} />
-          </View>
-        )} */}
+            {item.star === 1 && (
+              <View style={styles.iconOverlay}>
+                <StarIcon fill={Colors.WHITE} width={12} />
+              </View>
+            )}
 
             {item.joined && token ? (
               <View style={styles.joinedOverlay}>
@@ -241,7 +417,7 @@ const EventsScreen = () => {
             )}
           </View>
 
-          {item.type !== 1 || item.full ? (
+          {item.type === 2 || item.type === 3 || item.full ? (
             <View
               style={[
                 styles.statusBadge,
@@ -258,6 +434,73 @@ const EventsScreen = () => {
     );
   };
 
+  const renderScene = ({
+    route
+  }: {
+    route: { key: 'nm' | 'community' | 'data'; title: string };
+  }) => {
+    const isCurrentTabExpanded = expandedStates[route.key];
+
+    return (
+      <>
+        <FlashList
+          ref={(ref) => {
+            scrollViewRefs.current[route.key] = ref;
+          }}
+          data={filteredEvents[route.key] || []}
+          scrollEnabled={true}
+          keyExtractor={(item) => `${route.key}-${item.id}`}
+          renderItem={renderEventCard}
+          estimatedItemSize={120}
+          contentContainerStyle={styles.listContainer}
+          showsVerticalScrollIndicator={false}
+          onScroll={handleScroll}
+          scrollEventThrottle={16}
+          ListFooterComponent={
+            filteredPastEvents[route.key] && filteredPastEvents[route.key].length ? (
+              <View ref={sectionRef} style={styles.sectionContainer}>
+                <TouchableOpacity onPress={() => toggleExpand(route.key)} style={styles.header}>
+                  <View style={styles.headerContainer}>
+                    <Text style={styles.headerText}>Past Events</Text>
+                  </View>
+
+                  <View style={styles.chevronContainer}>
+                    <ChevronIcon
+                      fill={Colors.DARK_BLUE}
+                      style={[styles.headerIcon, isCurrentTabExpanded ? styles.rotate : null]}
+                    />
+                  </View>
+                </TouchableOpacity>
+
+                {isCurrentTabExpanded ? (
+                  <FlashList
+                    data={filteredPastEvents[route.key] || []}
+                    scrollEnabled={true}
+                    keyExtractor={(item) => item.id.toString()}
+                    renderItem={renderEventCard}
+                    estimatedItemSize={100}
+                    contentContainerStyle={styles.listContainer}
+                    showsVerticalScrollIndicator={false}
+                  />
+                ) : null}
+              </View>
+            ) : null
+          }
+        />
+      </>
+    );
+  };
+
+  const handleIndexChange = useCallback(
+    (newIndex: number) => {
+      setSearchQuery('');
+      if (newIndex >= 0 && newIndex < routes.length) {
+        setIndex(newIndex);
+      }
+    },
+    [routes.length]
+  );
+
   return (
     <PageWrapper>
       <Header
@@ -265,8 +508,10 @@ const EventsScreen = () => {
         rightElement={
           canAddEvent?.can ? (
             <TouchableOpacity
-              onPress={() => navigation.navigate(NAVIGATION_PAGES.CREATE_EVENT as never)}
+              ref={buttonRef}
+              onPress={handleAddButtonPress}
               style={{ width: 30 }}
+              hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
             >
               <CalendarPlusIcon fill={Colors.DARK_BLUE} />
             </TouchableOpacity>
@@ -274,58 +519,94 @@ const EventsScreen = () => {
         }
       />
 
-      <ScrollView ref={scrollViewRef} nestedScrollEnabled showsVerticalScrollIndicator={false}>
-        <View style={styles.searchContainer}>
-          <Input
-            inputMode={'search'}
-            placeholder={'Search'}
-            onChange={(text) => handleSearch(text)}
-            value={searchQuery}
-            icon={<SearchIcon fill={'#C8C8C8'} width={14} height={14} />}
-            height={38}
-          />
-        </View>
-
-        <FlashList
-          data={filteredEvents}
-          scrollEnabled={false}
-          keyExtractor={(item) => item.id.toString()}
-          renderItem={renderEventCard}
-          estimatedItemSize={100}
-          contentContainerStyle={styles.listContainer}
-          showsVerticalScrollIndicator={false}
-          extraData={tooltipStates}
+      {/* <ScrollView
+        ref={scrollViewRef}
+        nestedScrollEnabled
+        showsVerticalScrollIndicator={false}
+        style={{ flex: 1, height: '100%' }}
+      > */}
+      <Animated.View style={[styles.searchContainer, searchContainerAnimatedStyle]}>
+        <Input
+          inputMode={'search'}
+          placeholder={'Search'}
+          onChange={(text) => handleSearch(text)}
+          value={searchQuery}
+          icon={<SearchIcon fill={'#C8C8C8'} width={14} height={14} />}
+          height={38}
         />
+      </Animated.View>
 
-        {filteredPastEvents && filteredPastEvents.length ? (
-          <View ref={sectionRef} style={styles.sectionContainer}>
-            <TouchableOpacity onPress={toggleExpand} style={styles.header}>
-              <View style={styles.headerContainer}>
-                <Text style={styles.headerText}>Past Events</Text>
-              </View>
+      {/* <TabViewDelayed waitBeforeShow={300}> */}
+      <TabView
+        navigationState={{ index, routes }}
+        renderScene={renderScene}
+        onIndexChange={handleIndexChange}
+        lazy={false}
+        swipeEnabled={true}
+        renderTabBar={(props) => (
+          <TabBar
+            {...props}
+            indicatorStyle={{ backgroundColor: Colors.DARK_BLUE }}
+            style={styles.tabBar}
+            tabStyle={styles.tabStyle}
+            pressColor={'transparent'}
+            renderLabel={({ route, focused }) => (
+              <Text
+                style={[
+                  styles.tabLabel,
+                  { color: Colors.DARK_BLUE, opacity: focused ? 1 : 0.4, textAlign: 'center' }
+                ]}
+                numberOfLines={2}
+                adjustsFontSizeToFit={true}
+                minimumFontScale={0.8}
+              >
+                {route.title}
+              </Text>
+            )}
+          />
+        )}
+      />
+      {/* </TabViewDelayed> */}
+      {/* </ScrollView> */}
 
-              <View style={styles.chevronContainer}>
-                <ChevronIcon
-                  fill={Colors.DARK_BLUE}
-                  style={[styles.headerIcon, isExpanded ? styles.rotate : null]}
-                />
-              </View>
+      <Modal
+        visible={showPopup}
+        transparent={true}
+        animationType="fade"
+        onRequestClose={() => setShowPopup(false)}
+      >
+        <TouchableOpacity
+          style={{
+            flex: 1,
+            backgroundColor: 'rgba(0,0,0,0.1)'
+          }}
+          onPress={() => setShowPopup(false)}
+          activeOpacity={1}
+        >
+          <View
+            style={[
+              popupStyles.popup,
+              {
+                top: popupPosition.y,
+                left: popupPosition.x
+              }
+            ]}
+          >
+            <TouchableOpacity
+              style={popupStyles.popupOption}
+              onPress={() => handlePopupOption('meeting')}
+            >
+              <Text style={popupStyles.popupText}>Add meeting</Text>
+            </TouchableOpacity>
+            <TouchableOpacity
+              style={[popupStyles.popupOption, popupStyles.popupOptionLast]}
+              onPress={() => handlePopupOption('trip')}
+            >
+              <Text style={popupStyles.popupText}>Add shared trip</Text>
             </TouchableOpacity>
-
-            {isExpanded ? (
-              <FlashList
-                data={filteredPastEvents}
-                scrollEnabled={false}
-                keyExtractor={(item) => item.id.toString()}
-                renderItem={renderEventCard}
-                estimatedItemSize={100}
-                contentContainerStyle={styles.listContainer}
-                showsVerticalScrollIndicator={false}
-              />
-            ) : null}
           </View>
-        ) : null}
-      </ScrollView>
+        </TouchableOpacity>
+      </Modal>
     </PageWrapper>
   );
 };

+ 57 - 2
src/screens/InAppScreens/TravelsScreen/EventsScreen/styles.tsx

@@ -4,10 +4,11 @@ import { getFontSize } from 'src/utils';
 
 export const styles = StyleSheet.create({
   listContainer: {
-    paddingTop: 12
+    paddingTop: 12,
+    paddingBottom: 12
   },
   searchContainer: {
-    paddingBottom: 6
+    marginBottom: 6
   },
   card: {
     borderRadius: 8,
@@ -143,5 +144,59 @@ export const styles = StyleSheet.create({
   },
   rotate: {
     transform: [{ rotate: '90deg' }]
+  },
+  tabBar: {
+    backgroundColor: 'transparent',
+    elevation: 0,
+    shadowOpacity: 0,
+    marginTop: 0,
+    height: 48
+  },
+  tabLabel: {
+    color: 'grey',
+    fontSize: getFontSize(14),
+    fontWeight: '700',
+    padding: 8,
+    width: Dimensions.get('window').width / 3,
+    textAlign: 'center',
+    flexWrap: 'wrap'
+  },
+  tabStyle: {
+    padding: 0,
+    marginHorizontal: 2
+  }
+});
+
+export const popupStyles = StyleSheet.create({
+  popup: {
+    position: 'absolute',
+    backgroundColor: Colors.WHITE,
+    borderRadius: 8,
+    shadowColor: '#000',
+    shadowOffset: {
+      width: 0,
+      height: 2
+    },
+    shadowOpacity: 0.1,
+    shadowRadius: 3.84,
+    elevation: 5,
+    zIndex: 1000,
+    minWidth: 150,
+    borderWidth: 1,
+    borderColor: Colors.LIGHT_GRAY
+  },
+  popupOption: {
+    paddingVertical: 12,
+    paddingHorizontal: 16,
+    borderBottomWidth: 1,
+    borderBottomColor: Colors.FILL_LIGHT
+  },
+  popupOptionLast: {
+    borderBottomWidth: 0
+  },
+  popupText: {
+    fontSize: getFontSize(14),
+    color: Colors.DARK_BLUE,
+    fontFamily: 'redhat-600'
   }
 });

+ 4 - 2
src/types/api.ts

@@ -209,7 +209,8 @@ export enum API_ENDPOINT {
   GET_MAP_DATA_FOR_REGION = 'get-map-data-for-region',
   GET_LAST_MAP_UPDATE_DATE = 'get-last-map-update-date',
   GET_SIZE_FOR_BOUNDING_BOX = 'get-size-for-bounding-box',
-  GET_PROFILE_SERIES_DATA = 'get-profile-series-data'
+  GET_PROFILE_SERIES_DATA = 'get-profile-series-data',
+  ADD_SHARED_TRIP = 'add-shared-trip'
 }
 
 export enum API {
@@ -391,7 +392,8 @@ export enum API {
   GET_MAP_DATA_FOR_REGION = `${API_ROUTE.MAPS}/${API_ENDPOINT.GET_MAP_DATA_FOR_REGION}`,
   GET_LAST_MAP_UPDATE_DATE = `${API_ROUTE.MAPS}/${API_ENDPOINT.GET_LAST_MAP_UPDATE_DATE}`,
   GET_SIZE_FOR_BOUNDING_BOX = `${API_ROUTE.MAPS}/${API_ENDPOINT.GET_SIZE_FOR_BOUNDING_BOX}`,
-  GET_PROFILE_SERIES_DATA = `${API_ROUTE.PROFILE}/${API_ENDPOINT.GET_PROFILE_SERIES_DATA}`
+  GET_PROFILE_SERIES_DATA = `${API_ROUTE.PROFILE}/${API_ENDPOINT.GET_PROFILE_SERIES_DATA}`,
+  ADD_SHARED_TRIP = `${API_ROUTE.EVENTS}/${API_ENDPOINT.ADD_SHARED_TRIP}`
 }
 
 export type BaseAxiosError = AxiosError;

+ 2 - 1
src/types/navigation.ts

@@ -86,5 +86,6 @@ export enum NAVIGATION_PAGES {
   OFFLINE_SELECT_REGIONS = 'inAppOfflineSelectRegions',
   OFFLINE_DOWNLOAD_PROGRESS = 'inAppOfflineDownloadProgress',
   EDIT_COUNTRY_DATA = 'inAppEditCountryData',
-  IN_APP_EVENTS_TAB = 'Events'
+  IN_APP_EVENTS_TAB = 'Events',
+  CREATE_SHARED_TRIP = 'inAppCreateSharedTrip',
 }