Viktoriia пре 2 месеци
родитељ
комит
2ba40635dc

+ 5 - 0
Route.tsx

@@ -100,6 +100,7 @@ import FullMapScreen from 'src/screens/InAppScreens/MessagesScreen/FullMapScreen
 import EventScreen from 'src/screens/InAppScreens/TravelsScreen/EventScreen';
 import GroupChatScreen from 'src/screens/InAppScreens/MessagesScreen/GroupChatScreen';
 import GroupSettingScreen from 'src/screens/InAppScreens/MessagesScreen/GroupSettingsScreen';
+import CreateEventScreen from 'src/screens/InAppScreens/TravelsScreen/CreateEvent';
 import MembersListScreen from 'src/screens/InAppScreens/MessagesScreen/MembersListScreen';
 import AllEventPhotosScreen from 'src/screens/InAppScreens/TravelsScreen/AllEventPhotosScreen';
 
@@ -323,6 +324,10 @@ const Route = () => {
             <ScreenStack.Screen name={NAVIGATION_PAGES.ADD_FIXER} component={AddNewFixerScreen} />
             <ScreenStack.Screen name={NAVIGATION_PAGES.EVENTS} component={EventsScreen} />
             <ScreenStack.Screen name={NAVIGATION_PAGES.EVENT} component={EventScreen} />
+            <ScreenStack.Screen
+              name={NAVIGATION_PAGES.CREATE_EVENT}
+              component={CreateEventScreen}
+            />
             <ScreenStack.Screen
               name={NAVIGATION_PAGES.ALL_EVENT_PHOTOS}
               component={AllEventPhotosScreen}

+ 2 - 0
package.json

@@ -79,6 +79,7 @@
     "react-native-modal": "^13.0.1",
     "react-native-pager-view": "6.3.0",
     "react-native-paper": "^5.12.3",
+    "react-native-pell-rich-editor": "^1.9.0",
     "react-native-progress": "^5.0.1",
     "react-native-reanimated": "~3.10.1",
     "react-native-reanimated-carousel": "^3.5.1",
@@ -92,6 +93,7 @@
     "react-native-url-polyfill": "^2.0.0",
     "react-native-view-shot": "^3.7.0",
     "react-native-walkthrough-tooltip": "^1.6.0",
+    "react-native-webview": "13.8.6",
     "react-native-wheel-pick": "^1.2.2",
     "uuid": "^10.0.0",
     "yup": "^1.3.3",

+ 12 - 0
patches/react-native-pell-rich-editor+1.9.0.patch

@@ -0,0 +1,12 @@
+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 14bea22..792147f 100644
+--- a/node_modules/react-native-pell-rich-editor/src/RichEditor.js
++++ b/node_modules/react-native-pell-rich-editor/src/RichEditor.js
+@@ -267,7 +267,6 @@ export default class RichTextEditor extends Component {
+           ref={that.setRef}
+           onMessage={that.onMessage}
+           originWhitelist={['*']}
+-          dataDetectorTypes={'none'}
+           domStorageEnabled={false}
+           bounces={false}
+           javaScriptEnabled={true}

+ 11 - 1
src/components/FlatList/modal-flatlist/index.tsx

@@ -9,11 +9,19 @@ type Props = {
   selectedObject?: (object: any) => void;
   headerTitle: string;
   defaultObject?: { name?: string };
+  placeholder?: string;
+  formikError?: any;
 };
 
 //TODO: rework to generic types
 
-export const ModalFlatList: FC<Props> = ({ selectedObject, headerTitle, defaultObject }) => {
+export const ModalFlatList: FC<Props> = ({
+  selectedObject,
+  headerTitle,
+  defaultObject,
+  placeholder,
+  formikError
+}) => {
   const [visible, setVisible] = useState(false);
   const [selectedFlatListObject, setSelectedFlatListObject] = useState<{ name?: string }>({});
 
@@ -32,6 +40,8 @@ export const ModalFlatList: FC<Props> = ({ selectedObject, headerTitle, defaultO
         isFocused={(b) => setVisible(b)}
         value={selectedFlatListObject?.name ?? defaultObject?.name}
         inputMode={'none'}
+        placeholder={placeholder}
+        formikError={formikError}
       />
       <Modal
         onRequestClose={() => setVisible(false)}

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

@@ -86,20 +86,20 @@ export const Input: FC<Props> = ({
           multiline={multiline}
           contextMenuHidden={inputMode === 'none'}
           caretHidden={inputMode === 'none'}
-          onPressIn={() => {
-            if (inputMode === 'none') {
+          onPress={() => {
+            setFocused(true);
+            if (isFocused) {
+              isFocused(true);
+            }
+          }}
+          onFocus={() => {
+            if (inputMode !== 'none') {
               setFocused(true);
               if (isFocused) {
                 isFocused(true);
               }
             }
           }}
-          onFocus={() => {
-            setFocused(true);
-            if (isFocused) {
-              isFocused(true);
-            }
-          }}
           onBlur={(e) => {
             setFocused(false);
             if (onBlur) {

+ 19 - 1
src/modules/api/events/events-api.ts

@@ -114,6 +114,14 @@ export interface PostUploadTempFileReturn extends ResponseType {
   temp_name: string;
 }
 
+export interface PostGetCanAddEventReturn extends ResponseType {
+  can: boolean;
+}
+
+export interface PostGetPhotosForRegionReturn extends ResponseType {
+  photos: number[];
+}
+
 export interface PostJoinEvent {
   token: string;
   id: number;
@@ -154,6 +162,11 @@ export interface PostDeleteFile {
   event_id: number;
 }
 
+export interface PostAddEvent {
+  token: string;
+  event: any;
+}
+
 export const eventsApi = {
   getEventsList: (token: string) =>
     request.postForm<PostGetEventsListReturn>(API.GET_EVENTS_LIST, { token }),
@@ -190,5 +203,10 @@ export const eventsApi = {
     });
   },
   getMorePhotos: (token: string, event_id: number, last_id: number) =>
-    request.postForm<PostGetMorePhotosReturn>(API.GET_MORE_PHOTOS, { token, event_id, last_id })
+    request.postForm<PostGetMorePhotosReturn>(API.GET_MORE_PHOTOS, { token, event_id, last_id }),
+  canAddEvent: (token: string) =>
+    request.postForm<PostGetCanAddEventReturn>(API.CAN_ADD_EVENT, { token }),
+  getPhotosForRegion: (region_id: number) =>
+    request.postForm<PostGetPhotosForRegionReturn>(API.GET_PHOTOS_FOR_REGION, { region_id }),
+  addEvent: (data: PostAddEvent) => request.postForm<ResponseType>(API.ADD_EVENT, data)
 };

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

@@ -8,5 +8,8 @@ export const eventsQueryKeys = {
   deleteFile: () => ['deleteFile'] as const,
   uploadPhoto: () => ['uploadPhoto'] as const,
   getMorePhotos: (token: string, event_id: number, last_id: number) =>
-    ['getMorePhotos', { token, event_id, last_id }] as const
+    ['getMorePhotos', { token, event_id, last_id }] as const,
+  canAddEvent: (token: string) => ['canAddEvent', token] as const,
+  getPhotosForRegion: () => ['getPhotosForRegion'] as const,
+  addEvent: () => ['addEvent'] as const
 };

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

@@ -7,3 +7,6 @@ export * from './use-post-event-add-file';
 export * from './use-post-delete-file';
 export * from './use-post-upload-photo';
 export * from './use-post-get-more-photos';
+export * from './use-post-can-add-event';
+export * from './use-post-get-photos-for-region';
+export * from './use-post-add-event';

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

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

+ 17 - 0
src/modules/api/events/queries/use-post-can-add-event.tsx

@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { eventsQueryKeys } from '../events-query-keys';
+import { eventsApi, type PostGetCanAddEventReturn } from '../events-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const useGetCanAddEventQuery = (token: string, enabled: boolean) => {
+  return useQuery<PostGetCanAddEventReturn, BaseAxiosError>({
+    queryKey: eventsQueryKeys.canAddEvent(token),
+    queryFn: async () => {
+      const response = await eventsApi.canAddEvent(token);
+      return response.data;
+    },
+    enabled
+  });
+};

+ 21 - 0
src/modules/api/events/queries/use-post-get-photos-for-region.tsx

@@ -0,0 +1,21 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { eventsQueryKeys } from '../events-query-keys';
+import { type PostGetPhotosForRegionReturn, eventsApi } from '../events-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const usePostGetPhotosForRegionMutation = () => {
+  return useMutation<
+    PostGetPhotosForRegionReturn,
+    BaseAxiosError,
+    { region_id: number },
+    PostGetPhotosForRegionReturn
+  >({
+    mutationKey: eventsQueryKeys.getPhotosForRegion(),
+    mutationFn: async (variables) => {
+      const response = await eventsApi.getPhotosForRegion(variables.region_id);
+      return response.data;
+    }
+  });
+};

+ 245 - 0
src/screens/InAppScreens/TravelsScreen/Components/AddMapPinModal/index.tsx

@@ -0,0 +1,245 @@
+import React, { useRef, useState } from 'react';
+import { View, StyleSheet, TouchableOpacity, Text } from 'react-native';
+import ActionSheet, { SheetManager } from 'react-native-actions-sheet';
+import { Colors } from 'src/theme';
+import { GOOGLE_MAP_PLACES_APIKEY } from 'src/constants';
+
+import { getFontSize } from 'src/utils';
+import SearchIcon from 'assets/icons/search.svg';
+import {
+  GooglePlaceData,
+  GooglePlaceDetail,
+  GooglePlacesAutocomplete
+} from 'react-native-google-places-autocomplete';
+import MapView, { Marker } from 'react-native-maps';
+import axios from 'axios';
+
+const AddMapPinModal = () => {
+  const mapRef = useRef<MapView>(null);
+
+  const [data, setData] = useState<any>(null);
+  const [marker, setMarker] = useState<any>(null);
+
+  const handleSheetOpen = (payload: any) => {
+    setData(payload);
+  };
+
+  const animateMapToRegion = (latitude: number, longitude: number) => {
+    const region = {
+      latitude,
+      longitude,
+      latitudeDelta: 0.015,
+      longitudeDelta: 0.0121
+    };
+    mapRef.current?.animateToRegion(region, 500);
+  };
+
+  const handlePlaceSelection = (googleData: GooglePlaceData, details: GooglePlaceDetail | null) => {
+    if (details) {
+      const { geometry, address_components } = details;
+
+      const cityComponent = address_components?.find(
+        (component) =>
+          component.types.includes('locality') ||
+          component.types.includes('administrative_area_level_2')
+      );
+      const city = cityComponent?.long_name || null;
+
+      setMarker({
+        placeId: googleData.place_id,
+        name: googleData.structured_formatting.main_text,
+        coordinate: {
+          latitude: geometry.location.lat,
+          longitude: geometry.location.lng
+        },
+        url: details.url,
+        city: city
+      });
+      animateMapToRegion(geometry.location.lat, geometry.location.lng);
+    }
+  };
+
+  const findPlace = async (placeId: string) => {
+    const response = await axios.get(
+      `https://maps.googleapis.com/maps/api/place/details/json?place_id=${placeId}&language=en&fields=address_component,name,url&key=${GOOGLE_MAP_PLACES_APIKEY}`
+    );
+
+    const result = response.data.result;
+    const addressComponents = result.address_components || [];
+
+    const cityComponent = addressComponents.find(
+      (component: { types: string | string[] }) =>
+        component.types.includes('locality') ||
+        component.types.includes('administrative_area_level_2')
+    );
+
+    const city = cityComponent ? cityComponent?.long_name || null : null;
+
+    return { url: result.url, name: result.name, city: city };
+  };
+
+  const handlePoiClick = async (event: any) => {
+    const { placeId, coordinate } = event.nativeEvent;
+    const { url, name, city } = await findPlace(placeId);
+    setMarker({
+      placeId,
+      name,
+      coordinate,
+      url,
+      city
+    });
+    animateMapToRegion(coordinate.latitude, coordinate.longitude);
+  };
+
+  return (
+    <ActionSheet
+      id="add-map-pin-modal"
+      containerStyle={styles.sheetContainer}
+      defaultOverlayOpacity={0.5}
+      closeOnTouchBackdrop={true}
+      keyboardHandlerEnabled={false}
+      onBeforeShow={(sheetRef) => {
+        const payload = sheetRef || null;
+        handleSheetOpen(payload);
+      }}
+    >
+      <View style={styles.container}>
+        <View style={styles.header}>
+          <TouchableOpacity
+            style={{
+              paddingTop: 16,
+              paddingBottom: 6,
+              paddingHorizontal: 6
+            }}
+            onPress={() => {
+              SheetManager.hide('add-map-pin-modal');
+            }}
+          >
+            <Text style={styles.headerText}>Cancel</Text>
+          </TouchableOpacity>
+
+          {marker ? (
+            <TouchableOpacity
+              onPress={() => {
+                data?.setPin(marker);
+                SheetManager.hide('add-map-pin-modal');
+              }}
+              style={{
+                paddingTop: 16,
+                paddingBottom: 6,
+                paddingHorizontal: 6
+              }}
+            >
+              <Text style={styles.headerText}>Save</Text>
+            </TouchableOpacity>
+          ) : null}
+        </View>
+
+        <View style={styles.searchContainer}>
+          <GooglePlacesAutocomplete
+            placeholder="Find a spot"
+            onPress={handlePlaceSelection}
+            query={{
+              key: GOOGLE_MAP_PLACES_APIKEY,
+              language: 'en',
+              types: 'establishment'
+            }}
+            GooglePlacesDetailsQuery={{
+              language: 'en'
+            }}
+            nearbyPlacesAPI="GooglePlacesSearch"
+            fetchDetails={true}
+            styles={{
+              textInput: styles.searchInput
+            }}
+            isRowScrollable={true}
+            renderLeftButton={() => (
+              <View style={styles.searchIcon}>
+                <SearchIcon fill={Colors.LIGHT_GRAY} />
+              </View>
+            )}
+          />
+        </View>
+
+        <View style={styles.mapContainer}>
+          <MapView
+            ref={mapRef}
+            style={styles.map}
+            showsMyLocationButton={true}
+            showsUserLocation={true}
+            showsCompass={false}
+            zoomControlEnabled={false}
+            mapType={'standard'}
+            initialRegion={{
+              latitude: 0,
+              longitude: 0,
+              latitudeDelta: 180,
+              longitudeDelta: 180
+            }}
+            provider="google"
+            onPoiClick={handlePoiClick}
+          >
+            {marker && <Marker coordinate={marker.coordinate} />}
+          </MapView>
+        </View>
+      </View>
+    </ActionSheet>
+  );
+};
+
+const styles = StyleSheet.create({
+  map: {
+    ...StyleSheet.absoluteFillObject
+  },
+  searchContainer: {
+    gap: 16,
+    flexDirection: 'row',
+    alignItems: 'center'
+  },
+  searchInput: {
+    backgroundColor: Colors.FILL_LIGHT,
+    borderRadius: 0,
+    borderTopRightRadius: 4,
+    borderBottomRightRadius: 4,
+    height: 38
+  },
+  searchIcon: {
+    height: 36,
+    backgroundColor: Colors.FILL_LIGHT,
+    paddingLeft: 16,
+    alignItems: 'center',
+    justifyContent: 'center',
+    borderTopLeftRadius: 4,
+    borderBottomLeftRadius: 4
+  },
+  header: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center'
+  },
+  headerText: {
+    color: Colors.DARK_BLUE,
+    fontSize: getFontSize(14),
+    fontWeight: '700'
+  },
+  container: {
+    backgroundColor: 'white',
+    gap: 16,
+    height: '100%'
+  },
+  mapContainer: {
+    flex: 1,
+    justifyContent: 'center',
+    marginHorizontal: -16,
+    overflow: 'hidden'
+  },
+  sheetContainer: {
+    height: '95%',
+    borderTopLeftRadius: 15,
+    borderTopRightRadius: 15,
+    paddingHorizontal: 16,
+    overflow: 'hidden'
+  }
+});
+
+export default AddMapPinModal;

+ 109 - 0
src/screens/InAppScreens/TravelsScreen/Components/PhotosForRegionModal/PhotosForRegionModal.tsx

@@ -0,0 +1,109 @@
+import React, { useState } from 'react';
+import { View, StyleSheet, TouchableOpacity, Text, Image, FlatList } from 'react-native';
+import ActionSheet, { SheetManager } from 'react-native-actions-sheet';
+import { Colors } from 'src/theme';
+import { API_HOST } from 'src/constants';
+
+import { itemWidth } from '../../utils';
+import { getFontSize } from 'src/utils';
+
+const PhotosForRegionModal = () => {
+  const [data, setData] = useState<any>(null);
+
+  const handleSheetOpen = (payload: any) => {
+    setData(payload);
+  };
+
+  const renderItem = ({ item }: { item: any }) => (
+    <View style={{ alignItems: 'center', marginVertical: 8 }}>
+      <TouchableOpacity
+        style={{ position: 'relative' }}
+        onPress={() => {
+          data?.selectPhoto(item);
+          SheetManager.hide('photos-for-region-modal');
+        }}
+      >
+        <Image
+          source={{
+            uri: `${API_HOST}/webapi/photos/${item}/small`
+          }}
+          style={[styles.image, { width: itemWidth, height: itemWidth }]}
+        />
+      </TouchableOpacity>
+    </View>
+  );
+
+  return (
+    <ActionSheet
+      id="photos-for-region-modal"
+      containerStyle={styles.sheetContainer}
+      defaultOverlayOpacity={0.5}
+      closeOnTouchBackdrop={true}
+      keyboardHandlerEnabled={true}
+      onBeforeShow={(sheetRef) => {
+        const payload = sheetRef || null;
+        handleSheetOpen(payload);
+      }}
+    >
+      <View style={styles.container}>
+        <View style={styles.header}>
+          <TouchableOpacity
+            style={{
+              paddingTop: 16,
+              paddingBottom: 6,
+              paddingHorizontal: 6
+            }}
+            onPress={() => {
+              SheetManager.hide('photos-for-region-modal');
+            }}
+          >
+            <Text style={styles.cancelText}>Cancel</Text>
+          </TouchableOpacity>
+        </View>
+
+        {data?.photos?.length > 0 && (
+          <FlatList
+            data={data.photos}
+            renderItem={renderItem}
+            keyExtractor={(item) => item.toString()}
+            numColumns={2}
+            columnWrapperStyle={styles.columnWrapper}
+            showsVerticalScrollIndicator={false}
+          />
+        )}
+      </View>
+    </ActionSheet>
+  );
+};
+
+const styles = StyleSheet.create({
+  header: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center'
+  },
+  cancelText: {
+    color: Colors.DARK_BLUE,
+    fontSize: getFontSize(14),
+    fontWeight: '700'
+  },
+  columnWrapper: {
+    justifyContent: 'space-between'
+  },
+  image: {
+    borderRadius: 8
+  },
+  container: {
+    backgroundColor: 'white',
+    gap: 16,
+    height: '100%'
+  },
+  sheetContainer: {
+    height: '95%',
+    borderTopLeftRadius: 15,
+    borderTopRightRadius: 15,
+    paddingHorizontal: 16
+  }
+});
+
+export default PhotosForRegionModal;

+ 391 - 0
src/screens/InAppScreens/TravelsScreen/CreateEvent/index.tsx

@@ -0,0 +1,391 @@
+import React, { useCallback, 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 './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, Header, Input, PageWrapper } from 'src/components';
+import { Colors } from 'src/theme';
+
+import EarthIcon from 'assets/icons/travels-section/earth.svg';
+import NomadsIcon from 'assets/icons/bottom-navigation/travellers.svg';
+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 LocationIcon from 'assets/icons/bottom-navigation/map.svg';
+import { ModalFlatList } from 'src/components/FlatList/modal-flatlist';
+import { usePostAddEventMutation, usePostGetPhotosForRegionMutation } 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';
+
+const EventSchema = yup.object({
+  event_name: yup.string().required().min(3),
+  date: yup.date().nullable().required(),
+  capacity: yup.number().optional(),
+  region: yup.number().required(),
+  photo: yup.number().nullable().optional(),
+  city: yup.string().required(),
+  address: yup.string().optional(),
+  pin: yup.object().nullable().required(),
+  details: yup.string().optional()
+});
+
+const CreateEventScreen = () => {
+  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: addEvent } = usePostAddEventMutation();
+
+  const [isSubmitting, setIsSubmitting] = useState(false);
+  const [calendarVisible, setCalendarVisible] = useState(false);
+  const [isViewerVisible, setIsViewerVisible] = useState(false);
+  const [photos, setPhotos] = useState<any[]>([]);
+
+  const handleGetPhotosForRegion = useCallback(
+    async (regionId: number) => {
+      await getPhotosForRegion(
+        { region_id: regionId },
+        {
+          onSuccess: (res) => {
+            if (res.photos) {
+              setPhotos(res.photos);
+            }
+          }
+        }
+      );
+    },
+    [token]
+  );
+
+  return (
+    <PageWrapper>
+      <Header label="Add event" />
+
+      <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={{
+                event_name: '',
+                date: '',
+                capacity: '',
+                region: null,
+                photo: null,
+                city: '',
+                address: '',
+                pin: null,
+                details: ''
+              }}
+              onSubmit={async (values) => {
+                setIsSubmitting(true);
+
+                const newEvent: any = {
+                  title: values.event_name,
+                  region: values.region,
+                  address1: values.city,
+                  address2: values.address,
+                  date: values.date,
+                  lon: (values.pin as any)?.coordinate?.longitude,
+                  lat: (values.pin as any)?.coordinate?.latitude,
+                  details: values.details
+                };
+
+                if (values.photo) {
+                  newEvent.photo = values.photo;
+                }
+
+                if (values.capacity) {
+                  newEvent.capacity = Number(values.capacity);
+                }
+
+                await addEvent(
+                  { token, event: JSON.stringify(newEvent) },
+                  {
+                    onSuccess: (res) => {
+                      setIsSubmitting(false);
+                      navigation.goBack();
+                    },
+                    onError: (err) => {
+                      setIsSubmitting(false);
+                    }
+                  }
+                );
+              }}
+            >
+              {(props) => (
+                <View style={{ gap: 12 }}>
+                  <Input
+                    header={'Event name'}
+                    placeholder={'Add event name'}
+                    inputMode={'text'}
+                    onChange={props.handleChange('event_name')}
+                    value={props.values.event_name}
+                    onBlur={props.handleBlur('event_name')}
+                    formikError={props.touched.event_name && props.errors.event_name}
+                  />
+
+                  <Input
+                    header={'Date'}
+                    placeholder={'Add date'}
+                    inputMode={'none'}
+                    onChange={props.handleChange('date')}
+                    value={props.values.date}
+                    onBlur={props.handleBlur('date')}
+                    isFocused={() => setCalendarVisible(true)}
+                    formikError={props.touched.date && props.errors.date}
+                    icon={<CalendarSvg fill={Colors.LIGHT_GRAY} width={20} height={20} />}
+                  />
+
+                  <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}
+                    icon={<NomadsIcon fill={Colors.LIGHT_GRAY} width={20} height={20} />}
+                  />
+
+                  <ModalFlatList
+                    headerTitle={'NomadMania region'}
+                    selectedObject={(data) => {
+                      props.setFieldValue('region', data.id);
+                      props.setFieldValue('photo', null);
+                      handleGetPhotosForRegion(data.id);
+                    }}
+                    placeholder="Please choose a region"
+                    formikError={
+                      props.touched.region && !props.values.region ? props.errors.region : null
+                    }
+                  />
+
+                  {props.values.region && 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}
+                      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}
+
+                  <Input
+                    header={'City'}
+                    placeholder={'Add event city'}
+                    inputMode={'text'}
+                    onChange={props.handleChange('city')}
+                    value={props.values.city}
+                    onBlur={props.handleBlur('city')}
+                    formikError={props.touched.city && props.errors.city}
+                    icon={<EarthIcon fill={Colors.LIGHT_GRAY} width={20} height={20} />}
+                  />
+
+                  <Input
+                    header={'Address'}
+                    placeholder={'Add event address'}
+                    inputMode={'text'}
+                    onChange={props.handleChange('address')}
+                    value={props.values.address}
+                    onBlur={props.handleBlur('address')}
+                    formikError={props.touched.address && props.errors.address}
+                    icon={<LocationIcon fill={Colors.LIGHT_GRAY} width={20} height={20} />}
+                  />
+
+                  <Input
+                    header={'Map pin'}
+                    placeholder={'Choose from map'}
+                    inputMode={'none'}
+                    onChange={props.handleChange('pin')}
+                    value={(props.values.pin as any)?.name}
+                    onBlur={props.handleBlur('pin')}
+                    isFocused={() =>
+                      SheetManager.show('add-map-pin-modal', {
+                        payload: {
+                          setPin: (pin: any) => {
+                            props.setFieldValue('pin', pin);
+                            props.setFieldValue('city', pin?.city);
+                          }
+                        } as any
+                      })
+                    }
+                    formikError={props.touched.pin && !props.values.pin ? props.errors.pin : false}
+                    icon={<LocationIcon fill={Colors.LIGHT_GRAY} width={20} height={20} />}
+                  />
+
+                  <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
+                      }}
+                      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: 500,
+                          animated: true
+                        });
+                      }}
+                      placeholder="Add event 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
+                      }}
+                      onBlur={() => props.handleBlur('details')}
+                    />
+                  </View>
+
+                  <View style={{ marginTop: 15, marginBottom: 15, gap: 8 }}>
+                    <Button onPress={() => 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}
+                    closeModal={(startDate?: string | null, endDate?: string | null) => {
+                      startDate && props.handleChange('date')(startDate.toString());
+                      setCalendarVisible(false);
+                    }}
+                    allowRangeSelection={false}
+                    selectedDate={props.values.date}
+                  />
+                  <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 />
+    </PageWrapper>
+  );
+};
+
+export default CreateEventScreen;

+ 5 - 0
src/screens/InAppScreens/TravelsScreen/CreateEvent/styles.tsx

@@ -0,0 +1,5 @@
+import { Dimensions, StyleSheet } from 'react-native';
+import { Colors } from '../../../../theme';
+import { getFontSize } from 'src/utils';
+
+export const styles = StyleSheet.create({});

+ 12 - 9
src/screens/InAppScreens/TravelsScreen/EventsScreen/index.tsx

@@ -16,7 +16,7 @@ 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, useGetEventsListQuery } from '@api/events';
+import { 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';
@@ -25,6 +25,7 @@ import { renderSpotsText } from './utils';
 const EventsScreen = () => {
   const token = (storage.get('token', StoreType.STRING) as string) ?? null;
   const { data, refetch } = useGetEventsListQuery(token, true);
+  const { data: canAddEvent } = useGetCanAddEventQuery(token, true);
   const navigation = useNavigation();
   const [searchQuery, setSearchQuery] = useState('');
   const [events, setEvents] = useState<SingleEvent[]>([]);
@@ -168,14 +169,16 @@ const EventsScreen = () => {
     <PageWrapper>
       <Header
         label="Events"
-        // rightElement={
-        //   <TouchableOpacity
-        //     onPress={() => navigation.navigate(NAVIGATION_PAGES.CREATE_EVENT as never)}
-        //     style={{ width: 30 }}
-        //   >
-        //     <CalendarPlusIcon fill={Colors.DARK_BLUE} />
-        //   </TouchableOpacity>
-        // }
+        rightElement={
+          canAddEvent?.can ? (
+            <TouchableOpacity
+              onPress={() => navigation.navigate(NAVIGATION_PAGES.CREATE_EVENT as never)}
+              style={{ width: 30 }}
+            >
+              <CalendarPlusIcon fill={Colors.DARK_BLUE} />
+            </TouchableOpacity>
+          ) : null
+        }
       />
 
       <View style={styles.searchContainer}>

+ 8 - 2
src/types/api.ts

@@ -195,7 +195,10 @@ export enum API_ENDPOINT {
   EVENT_ADD_FILE = 'event-add-file',
   DELETE_FILE = 'delete-file',
   UPLOAD_PHOTO_EVENT = 'upload-photo',
-  GET_MORE_PHOTOS = 'get-more-photos'
+  GET_MORE_PHOTOS = 'get-more-photos',
+  CAN_ADD_EVENT = 'can-add-event',
+  GET_PHOTOS_FOR_REGION = 'get-photos-for-region',
+  ADD_EVENT = 'add-event'
 }
 
 export enum API {
@@ -363,7 +366,10 @@ export enum API {
   EVENT_ADD_FILE = `${API_ROUTE.EVENTS}/${API_ENDPOINT.EVENT_ADD_FILE}`,
   DELETE_FILE = `${API_ROUTE.EVENTS}/${API_ENDPOINT.DELETE_FILE}`,
   UPLOAD_PHOTO_EVENT = `${API_ROUTE.EVENTS}/${API_ENDPOINT.UPLOAD_PHOTO_EVENT}`,
-  GET_MORE_PHOTOS = `${API_ROUTE.EVENTS}/${API_ENDPOINT.GET_MORE_PHOTOS}`
+  GET_MORE_PHOTOS = `${API_ROUTE.EVENTS}/${API_ENDPOINT.GET_MORE_PHOTOS}`,
+  CAN_ADD_EVENT = `${API_ROUTE.EVENTS}/${API_ENDPOINT.CAN_ADD_EVENT}`,
+  GET_PHOTOS_FOR_REGION = `${API_ROUTE.EVENTS}/${API_ENDPOINT.GET_PHOTOS_FOR_REGION}`,
+  ADD_EVENT = `${API_ROUTE.EVENTS}/${API_ENDPOINT.ADD_EVENT}`
 }
 
 export type BaseAxiosError = AxiosError;

+ 1 - 0
src/types/navigation.ts

@@ -76,6 +76,7 @@ export enum NAVIGATION_PAGES {
   EVENT = 'inAppEvent',
   FULL_MAP_VIEW = 'inAppFullMapView',
   GROUP_SETTINGS = 'inAppGroupSettings',
+  CREATE_EVENT = 'inAppCreateEvent',
   MEMBERS_LIST = 'inAppMembersList',
   ALL_EVENT_PHOTOS = 'inAppAllEventPhotos'
 }