Jelajahi Sumber

event fixes

Viktoriia 1 bulan lalu
induk
melakukan
f8b10cf03d

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

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

+ 8 - 4
src/components/Calendar/InputTimePicker/index.tsx

@@ -14,13 +14,17 @@ type Props = {
   formikError?: string | boolean;
   headerTitle?: string;
   defaultTime?: Date | null;
+  placeholder?: string;
+  inputHeader?: string;
 };
 
 export const InputTimePicker: FC<Props> = ({
   selectedTime,
   formikError,
   headerTitle,
-  defaultTime
+  defaultTime,
+  placeholder,
+  inputHeader
 }) => {
   const [visible, setVisible] = useState(false);
   const [spinnerSelectedTime, setSpinnerSelectedTime] = useState<Date | null>(defaultTime ?? null);
@@ -41,10 +45,10 @@ export const InputTimePicker: FC<Props> = ({
   };
 
   return (
-    <View>
+    <View style={{ flex: 1 }}>
       <Input
         icon={<ClockIcon fill={Colors.LIGHT_GRAY} width={20} height={20} />}
-        header={'Time'}
+        header={inputHeader ?? 'Time'}
         value={
           defaultTime
             ? defaultTime.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
@@ -53,7 +57,7 @@ export const InputTimePicker: FC<Props> = ({
         isFocused={(b) => setVisible(b)}
         onBlur={() => {}}
         inputMode={'none'}
-        placeholder={'Choose a time'}
+        placeholder={placeholder ?? 'Choose a time'}
         formikError={formikError}
       />
       <Modal

+ 33 - 4
src/modules/api/events/events-api.ts

@@ -26,8 +26,10 @@ export type SingleEvent = {
   available?: number;
   joined: 0 | 1;
   visible?: 0 | 1;
-  archived?: 0 | 1;
+  archived: 0 | 1;
   time?: string;
+  time_from?: string;
+  time_to?: string;
 };
 
 export type Participants = {
@@ -66,6 +68,7 @@ export type EventSettings = {
     nm: number | null;
     un: number | null;
   };
+  nm_region?: number;
 };
 
 export type EventAttachments = {
@@ -125,6 +128,21 @@ export interface PostGetPhotosForRegionReturn extends ResponseType {
   photos: number[];
 }
 
+export interface PostGetEventForEditingReturn {
+  address1: string;
+  address2: string;
+  capacity: number | null;
+  date: string;
+  details: string;
+  lat: number | null;
+  lon: number;
+  region: number;
+  time_from: string;
+  time_to: string;
+  title: string;
+  result: string;
+}
+
 export interface PostJoinEvent {
   token: string;
   id: number;
@@ -170,9 +188,15 @@ export interface PostAddEvent {
   event: any;
 }
 
+export interface PostUpdateEvent {
+  token: string;
+  event_id: number;
+  event: any;
+}
+
 export const eventsApi = {
-  getEventsList: (token: string) =>
-    request.postForm<PostGetEventsListReturn>(API.GET_EVENTS_LIST, { token }),
+  getEventsList: (token: string, past: number) =>
+    request.postForm<PostGetEventsListReturn>(API.GET_EVENTS_LIST, { token, past }),
   getEvent: (token: string, url: string) =>
     request.postForm<PostGetEventReturn>(API.GET_EVENT, { token, url }),
   joinEvent: (data: PostJoinEvent) => request.postForm<ResponseType>(API.JOIN_EVENT, data),
@@ -211,5 +235,10 @@ export const eventsApi = {
     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)
+  addEvent: (data: PostAddEvent) => request.postForm<ResponseType>(API.ADD_EVENT, data),
+  getEventForEditing: (token: string, event_id: number) =>
+    request.postForm<PostGetEventForEditingReturn>(API.GET_EVENT_FOR_EDITING, { token, event_id }),
+  updateEvent: (data: PostUpdateEvent) => request.postForm<ResponseType>(API.UPDATE_EVENT, data),
+  cancelEvent: (token: string, event_id: number) =>
+    request.postForm<ResponseType>(API.CANCEL_EVENT, { token, event_id })
 };

+ 6 - 2
src/modules/api/events/events-query-keys.tsx

@@ -1,5 +1,5 @@
 export const eventsQueryKeys = {
-  getEventsList: (token: string) => ['getEventsList', token] as const,
+  getEventsList: (token: string, past: number) => ['getEventsList', token, past] as const,
   getEvent: (token: string, url: string) => ['getEvent', { token, url }] as const,
   joinEvent: () => ['joinEvent'] as const,
   unjoinEvent: () => ['unjoinEvent'] as const,
@@ -11,5 +11,9 @@ export const eventsQueryKeys = {
     ['getMorePhotos', { token, event_id, last_id }] as const,
   canAddEvent: (token: string) => ['canAddEvent', token] as const,
   getPhotosForRegion: () => ['getPhotosForRegion'] as const,
-  addEvent: () => ['addEvent'] as const
+  getPhotosForRegionQuery: (region_id: number) => ['getPhotosForRegionQuery', region_id] as const,
+  addEvent: () => ['addEvent'] as const,
+  getEventForEditing: () => ['getEventForEditing'] as const,
+  updateEvent: () => ['updateEvent'] as const,
+  cancelEvent: () => ['cancelEvent'] as const,
 };

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

@@ -10,3 +10,6 @@ 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';
+export * from './use-post-get-event-for-editing';
+export * from './use-post-update-event';
+export * from './use-post-cancel-event';

+ 22 - 0
src/modules/api/events/queries/use-post-cancel-event.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 usePostCancelEventMutation = () => {
+  return useMutation<
+    ResponseType,
+    BaseAxiosError,
+    { token: string; event_id: number },
+    ResponseType
+  >({
+    mutationKey: eventsQueryKeys.cancelEvent(),
+    mutationFn: async (data) => {
+      const response = await eventsApi.cancelEvent(data.token, data.event_id);
+      return response.data;
+    }
+  });
+};

+ 22 - 0
src/modules/api/events/queries/use-post-get-event-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 useGetEventForEditingMutation = () => {
+  return useMutation<
+    PostGetEventForEditingReturn,
+    BaseAxiosError,
+    { token: string; event_id: number },
+    PostGetEventForEditingReturn
+  >({
+    mutationKey: eventsQueryKeys.getEventForEditing(),
+    mutationFn: async (data) => {
+      const response = await eventsApi.getEventForEditing(data.token, data.event_id);
+      return response.data;
+    }
+  });
+};

+ 3 - 3
src/modules/api/events/queries/use-post-get-event-list.tsx

@@ -5,11 +5,11 @@ import { eventsApi, type PostGetEventsListReturn } from '../events-api';
 
 import type { BaseAxiosError } from '../../../../types';
 
-export const useGetEventsListQuery = (token: string, enabled: boolean) => {
+export const useGetEventsListQuery = (token: string, past: 0 | 1, enabled: boolean) => {
   return useQuery<PostGetEventsListReturn, BaseAxiosError>({
-    queryKey: eventsQueryKeys.getEventsList(token),
+    queryKey: eventsQueryKeys.getEventsList(token, past),
     queryFn: async () => {
-      const response = await eventsApi.getEventsList(token);
+      const response = await eventsApi.getEventsList(token, past);
       return response.data;
     },
     enabled

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

@@ -1,4 +1,5 @@
 import { useMutation } from '@tanstack/react-query';
+import { useQuery } from '@tanstack/react-query';
 
 import { eventsQueryKeys } from '../events-query-keys';
 import { type PostGetPhotosForRegionReturn, eventsApi } from '../events-api';
@@ -19,3 +20,14 @@ export const usePostGetPhotosForRegionMutation = () => {
     }
   });
 };
+
+export const useGetPhotosForRegionQuery = (region_id: number, enabled: boolean) => {
+  return useQuery<PostGetPhotosForRegionReturn, BaseAxiosError>({
+    queryKey: eventsQueryKeys.getPhotosForRegionQuery(region_id),
+    queryFn: async () => {
+      const response = await eventsApi.getPhotosForRegion(region_id);
+      return response.data;
+    },
+    enabled
+  });
+};

+ 17 - 0
src/modules/api/events/queries/use-post-update-event.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 usePostUpdateEventMutation = () => {
+  return useMutation<ResponseType, BaseAxiosError, PostUpdateEvent, ResponseType>({
+    mutationKey: eventsQueryKeys.updateEvent(),
+    mutationFn: async (data) => {
+      const response = await eventsApi.updateEvent(data);
+      return response.data;
+    }
+  });
+};

+ 4 - 2
src/screens/InAppScreens/MapScreen/RegionViewScreen/ImageCarousel/index.tsx

@@ -13,12 +13,14 @@ const ImageCarousel = ({
   photos,
   activeIndex,
   setActiveIndex,
-  openModal
+  openModal,
+  containerStyles
 }: {
   photos: PhotosData[];
   activeIndex: number;
   setActiveIndex: (index: number) => void;
   openModal: (index: number) => void;
+  containerStyles?: any;
 }) => {
   const animationStyle = React.useCallback((value: number) => {
     'worklet';
@@ -50,7 +52,7 @@ const ImageCarousel = ({
         loop={false}
       />
 
-      <View style={styles.pagination}>
+      <View style={[styles.pagination, containerStyles && containerStyles]}>
         <PaginationDot
           activeDotColor={Colors.DARK_BLUE}
           inactiveDotColor={'rgba(15, 63, 79, 0.5)'}

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

@@ -22,6 +22,9 @@ const AddMapPinModal = () => {
 
   const handleSheetOpen = (payload: any) => {
     setData(payload);
+    if (payload?.pin && payload.pin?.coordinate) {
+      setMarker(payload.pin);
+    }
   };
 
   const animateMapToRegion = (latitude: number, longitude: number) => {

+ 222 - 65
src/screens/InAppScreens/TravelsScreen/CreateEvent/index.tsx

@@ -1,4 +1,4 @@
-import React, { useCallback, useRef, useState } from 'react';
+import React, { useCallback, useEffect, useRef, useState } from 'react';
 import {
   View,
   Text,
@@ -20,7 +20,7 @@ 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 { Button, Header, Input, Loading, PageWrapper, WarningModal } from 'src/components';
 import { Colors } from 'src/theme';
 
 import EarthIcon from 'assets/icons/travels-section/earth.svg';
@@ -31,17 +31,24 @@ 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 {
+  usePostAddEventMutation,
+  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 { InputTimePicker } from 'src/components/Calendar/InputTimePicker';
+import { NAVIGATION_PAGES } from 'src/types';
 
 const EventSchema = yup.object({
   event_name: yup.string().required().min(3),
   date: yup.date().nullable().required(),
-  time: yup.date().nullable().required(),
+  time_from: yup.date().nullable().required(),
+  time_to: yup.date().nullable().required(),
   capacity: yup.number().optional(),
   region: yup.number().required(),
   photo: yup.number().nullable().optional(),
@@ -51,19 +58,69 @@ const EventSchema = yup.object({
   details: yup.string().required()
 });
 
-const CreateEventScreen = () => {
+const CreateEventScreen = ({ route }: { route: any }) => {
+  const eventId = route.params?.eventId;
+  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: addEvent } = usePostAddEventMutation();
+  const { mutateAsync: updateEvent } = usePostUpdateEventMutation();
+  const { mutateAsync: cancelEvent } = usePostCancelEventMutation();
 
   const [isSubmitting, setIsSubmitting] = useState(false);
   const [calendarVisible, setCalendarVisible] = useState(false);
   const [isViewerVisible, setIsViewerVisible] = useState(false);
   const [photos, setPhotos] = useState<any[]>([]);
 
+  function timeStringToDate(timeStr: string): Date {
+    const [hours, minutes] = timeStr.split(':').map(Number);
+    const date = new Date();
+
+    date.setHours(hours, minutes, 0, 0);
+    return date;
+  }
+
+  const initialData: any = {
+    event_name: eventData?.title ?? '',
+    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 ?? '',
+    region: eventData?.region ?? null,
+    photo: null,
+    city: eventData?.address1 ?? '',
+    address: eventData?.address2 ?? '',
+    pin:
+      eventData && eventData?.lat
+        ? {
+            coordinate: {
+              longitude: eventData?.lon,
+              latitude: eventData?.lat
+            },
+            name: eventData?.address2
+          }
+        : null,
+    details: eventData?.details ?? ''
+  };
+
+  const [modalInfo, setModalInfo] = useState({
+    visible: false,
+    type: 'success',
+    title: '',
+    message: '',
+    buttonTitle: 'OK',
+    action: () => {}
+  });
+
+  useEffect(() => {
+    if (eventData && eventData.region) {
+      handleGetPhotosForRegion(eventData.region);
+    }
+  }, [eventData]);
+
   const handleGetPhotosForRegion = useCallback(
     async (regionId: number) => {
       await getPhotosForRegion(
@@ -80,6 +137,26 @@ const CreateEventScreen = () => {
     [token]
   );
 
+  const handleCancelEvent = () => {
+    setModalInfo({
+      visible: true,
+      type: 'delete',
+      title: 'Cancel event',
+      buttonTitle: 'Cancel',
+      message: `Are you sure you want to cancel this event?`,
+      action: () => {
+        cancelEvent(
+          { token, event_id: eventId },
+          {
+            onSuccess: (res) => {
+              navigation.navigate(NAVIGATION_PAGES.EVENTS as never);
+            }
+          }
+        );
+      }
+    });
+  };
+
   return (
     <PageWrapper>
       <Header label="Add event" />
@@ -92,18 +169,7 @@ const CreateEventScreen = () => {
           <ScrollView ref={scrollRef} showsVerticalScrollIndicator={false}>
             <Formik
               validationSchema={EventSchema}
-              initialValues={{
-                event_name: '',
-                date: '',
-                time: null,
-                capacity: '',
-                region: null,
-                photo: null,
-                city: '',
-                address: '',
-                pin: null,
-                details: ''
-              }}
+              initialValues={initialData}
               onSubmit={async (values) => {
                 setIsSubmitting(true);
 
@@ -124,27 +190,58 @@ const CreateEventScreen = () => {
 
                 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([], {
+                    hour: '2-digit',
+                    minute: '2-digit'
+                  });
                 }
 
-                if (values.time) {
-                  newEvent.time = (values.time as Date).toLocaleTimeString([], {
+                if (values.time_to) {
+                  newEvent.time_to = (values.time_to as Date).toLocaleTimeString([], {
                     hour: '2-digit',
                     minute: '2-digit'
                   });
                 }
 
-                await addEvent(
-                  { token, event: JSON.stringify(newEvent) },
-                  {
-                    onSuccess: (res) => {
-                      setIsSubmitting(false);
-                      navigation.goBack();
-                    },
-                    onError: (err) => {
-                      setIsSubmitting(false);
+                if (eventId) {
+                  await updateEvent(
+                    { token, event_id: eventId, event: JSON.stringify(newEvent) },
+                    {
+                      onSuccess: (res) => {
+                        setIsSubmitting(false);
+                        navigation.goBack();
+                      },
+                      onError: (err) => {
+                        setIsSubmitting(false);
+                      }
+                    }
+                  );
+                } else {
+                  await addEvent(
+                    { token, event: JSON.stringify(newEvent) },
+                    {
+                      onSuccess: (res) => {
+                        setIsSubmitting(false);
+                        setModalInfo({
+                          visible: true,
+                          type: 'success',
+                          title: 'Success',
+                          buttonTitle: 'OK',
+                          message: `Thank you for adding this new meeting. It will undergo review soon. You'll be notified via email once the meeting is approved.`,
+                          action: () => {}
+                        });
+                      },
+                      onError: (err) => {
+                        setIsSubmitting(false);
+                      }
                     }
-                  }
-                );
+                  );
+                }
               }}
             >
               {(props) => (
@@ -171,12 +268,25 @@ const CreateEventScreen = () => {
                     icon={<CalendarSvg fill={Colors.LIGHT_GRAY} width={20} height={20} />}
                   />
 
-                  <InputTimePicker
-                    headerTitle={'Select Time'}
-                    defaultTime={props.values.time}
-                    selectedTime={(time) => props.setFieldValue('time', time)}
-                    formikError={props.touched.time && props.errors.time}
-                  />
+                  <View style={{ flexDirection: 'row', gap: 8 }}>
+                    <InputTimePicker
+                      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}
+                      inputHeader={'Start time'}
+                      placeholder="Choose a time"
+                    />
+
+                    <InputTimePicker
+                      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}
+                      inputHeader={'End time'}
+                      placeholder="Choose a time"
+                    />
+                  </View>
 
                   <Input
                     header={'Maximum capacity'}
@@ -189,18 +299,27 @@ const CreateEventScreen = () => {
                     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
-                    }
-                  />
+                  {eventId ? (
+                    <Input
+                      header={'NomadMania region'}
+                      inputMode={'none'}
+                      editable={false}
+                      value={eventData?.region_name}
+                    />
+                  ) : (
+                    <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
@@ -279,7 +398,8 @@ const CreateEventScreen = () => {
                           setPin: (pin: any) => {
                             props.setFieldValue('pin', pin);
                             props.setFieldValue('city', pin?.city);
-                          }
+                          },
+                          pin: props.values.pin
                         } as any
                       })
                     }
@@ -388,23 +508,44 @@ const CreateEventScreen = () => {
                     ) : null}
                   </View>
 
-                  <View style={{ marginTop: 15, marginBottom: 15, gap: 8 }}>
-                    <Button onPress={() => props.handleSubmit()} disabled={isSubmitting}>
-                      Save
-                    </Button>
+                  {eventId ? (
+                    <View style={{ marginTop: 15, marginBottom: 15, gap: 8 }}>
+                      <Button onPress={() => props.handleSubmit()} disabled={isSubmitting}>
+                        Update event
+                      </Button>
+
+                      <Button
+                        variant={ButtonVariants.OPACITY}
+                        containerStyles={{
+                          backgroundColor: Colors.RED,
+                          borderColor: Colors.RED
+                        }}
+                        textStyles={{ color: Colors.WHITE }}
+                        onPress={handleCancelEvent}
+                      >
+                        Cancel event
+                      </Button>
+                    </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>
+                  )}
 
-                    <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) => {
@@ -435,6 +576,22 @@ const CreateEventScreen = () => {
       </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>
   );
 };

+ 121 - 37
src/screens/InAppScreens/TravelsScreen/EventScreen/index.tsx

@@ -24,6 +24,7 @@ import { API_HOST, APP_VERSION } from 'src/constants';
 import { StoreType, storage } from 'src/storage';
 import { MaterialCommunityIcons } from '@expo/vector-icons';
 import * as Progress from 'react-native-progress';
+import ImageView from 'better-react-native-image-viewing';
 
 import ChevronLeft from 'assets/icons/chevron-left.svg';
 import MapSvg from 'assets/icons/travels-screens/map-location.svg';
@@ -43,9 +44,12 @@ import {
   EventAttachments,
   EventData,
   EventPhotos,
+  useGetEventForEditingMutation,
   useGetEventQuery,
+  useGetPhotosForRegionQuery,
   usePostDeleteFileMutation,
   usePostEventAddFileMutation,
+  usePostGetPhotosForRegionMutation,
   usePostJoinEventMutation,
   usePostUnjoinEventMutation,
   usePostUploadPhotoMutation,
@@ -65,6 +69,9 @@ import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view
 import Tooltip from 'react-native-walkthrough-tooltip';
 import WebView from 'react-native-webview';
 import ClockIcon from 'assets/icons/events/clock.svg';
+import { PhotosData } from '../../MapScreen/RegionViewScreen/types';
+import ImageCarousel from '../../MapScreen/RegionViewScreen/ImageCarousel';
+import EditSvg from 'assets/icons/events/edit.svg';
 
 type TempFile = {
   filetype: string;
@@ -80,7 +87,7 @@ const fileWidth = Dimensions.get('window').width / 5;
 const EventScreen = ({ route }: { route: any }) => {
   const eventUrl = route.params?.url;
   const token = (storage.get('token', StoreType.STRING) as string) ?? null;
-  const currentUserId = (storage.get('uid', StoreType.NUMBER) as number) ?? 0;
+  const currentUserId = (storage.get('uid', StoreType.STRING) as string) ?? 0;
   const navigation = useNavigation();
   const { width: windowWidth } = useWindowDimensions();
   const contentWidth = windowWidth * 0.9;
@@ -93,6 +100,7 @@ const EventScreen = ({ route }: { route: any }) => {
   const { mutateAsync: saveFile } = usePostEventAddFileMutation();
   const { mutateAsync: deleteFile } = usePostDeleteFileMutation();
   const { mutateAsync: uploadPhoto } = usePostUploadPhotoMutation();
+  const { mutateAsync: getForEditing } = useGetEventForEditingMutation();
 
   const [isExpanded, setIsExpanded] = useState(false);
   const [tooltipUser, setTooltipUser] = useState<number | null>(null);
@@ -113,6 +121,13 @@ const EventScreen = ({ route }: { route: any }) => {
   const [myFiles, setMyFiles] = useState<EventAttachments[]>([]);
   const [photos, setPhotos] = useState<(EventPhotos & { isSending?: boolean })[]>([]);
   const [isUploading, setIsUploading] = useState(false);
+  const [photosForRegion, setPhotosForRegion] = useState<{ uriSmall: string; uri: string }[]>([]);
+  const [activeIndex, setActiveIndex] = useState(0);
+  const [isImageModalVisible, setIsImageModalVisible] = useState(false);
+  const [currentImageIndex, setCurrentImageIndex] = useState(0);
+  const [nmId, setNmId] = useState<number | null>(null);
+
+  const { data: photosData } = useGetPhotosForRegionQuery(nmId ?? 0, nmId !== null);
 
   const [modalInfo, setModalInfo] = useState({
     visible: false,
@@ -123,10 +138,21 @@ const EventScreen = ({ route }: { route: any }) => {
     action: () => {}
   });
 
+  useEffect(() => {
+    photosData &&
+      setPhotosForRegion(
+        photosData?.photos?.map((item) => ({
+          uriSmall: `${API_HOST}/ajax/pic/${item}/small`,
+          uri: `${API_HOST}/ajax/pic/${item}/full`
+        })) ?? []
+      );
+  }, [photosData]);
+
   useEffect(() => {
     if (data && data.data) {
       setEvent(data.data);
       setJoined(data.data.joined);
+      setNmId(data.data.settings.nm_region ?? null);
 
       setMyFiles(data.data.files ?? []);
       setPhotos(data.data.photos);
@@ -164,6 +190,11 @@ const EventScreen = ({ route }: { route: any }) => {
     }, [navigation])
   );
 
+  const openModal = (index: number) => {
+    setCurrentImageIndex(index);
+    setIsImageModalVisible(true);
+  };
+
   const handlePreviewDocument = useCallback(async (url: string, fileName: string) => {
     try {
       const dirExist = await FileSystem.getInfoAsync(CACHED_ATTACHMENTS_DIR);
@@ -357,7 +388,7 @@ const EventScreen = ({ route }: { route: any }) => {
         type: 'success',
         title: 'Success',
         buttonTitle: 'OK',
-        message: `Thank you for joing, we’ll get back to you soon.`,
+        message: `Thank you for joining, we’ll get back to you soon.`,
         action: () => {}
       });
     }
@@ -549,7 +580,7 @@ const EventScreen = ({ route }: { route: any }) => {
   };
 
   const formatEventDate = (event: EventData) => {
-    if (event.settings.date_from && event.settings.date_to) {
+    if (event.date_from && event.date_to) {
       return (
         <View>
           <Text
@@ -575,7 +606,7 @@ const EventScreen = ({ route }: { route: any }) => {
         </View>
       );
     } else {
-      let date = moment(event.settings.date, 'YYYY-MM-DD').format('DD MMMM YYYY');
+      let date = moment(event.date, 'YYYY-MM-DD').format('DD MMMM YYYY');
 
       return (
         <View>
@@ -588,7 +619,20 @@ const EventScreen = ({ route }: { route: any }) => {
           >
             {date}
           </Text>
-          {event.time && (
+          {event.time_from && event.time_to ? (
+            <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
+              <ClockIcon fill={Colors.DARK_BLUE} height={12} width={12} />
+              <Text
+                style={{
+                  fontSize: getFontSize(12),
+                  fontWeight: '600',
+                  color: Colors.DARK_BLUE
+                }}
+              >
+                {event.time_from} - {event.time_to}
+              </Text>
+            </View>
+          ) : event.time ? (
             <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
               <ClockIcon fill={Colors.DARK_BLUE} height={12} width={12} />
               <Text
@@ -601,7 +645,7 @@ const EventScreen = ({ route }: { route: any }) => {
                 {event.time}
               </Text>
             </View>
-          )}
+          ) : null}
         </View>
       );
     }
@@ -710,35 +754,17 @@ const EventScreen = ({ route }: { route: any }) => {
           showsVerticalScrollIndicator={false}
           removeClippedSubviews={false}
         >
-          <Image source={{ uri: photoUrl }} style={{ width: '100%', height: 220 }} />
-
-          {/* <TouchableOpacity
-            onPress={() => {
-              //     navigation.dispatch(
-              //       CommonActions.reset({
-              //         index: 1,
-              //         routes: [
-              //           {
-              //             name: NAVIGATION_PAGES.IN_APP_MAP_TAB,
-              //             state: {
-              //               routes: [
-              //                 {
-              //                   name: NAVIGATION_PAGES.MAP_TAB,
-              //                   params: { id: regionId, type: type === 'nm' ? 'regions' : 'places' }
-              //                 }
-              //               ]
-              //             }
-              //           }
-              //         ]
-              //       })
-              //     )
-            }}
-            style={styles.goToMapBtn}
-          >
-            <View style={styles.chevronWrapper}>
-              <MapSvg fill={Colors.WHITE} />
-            </View>
-          </TouchableOpacity> */}
+          {event.settings.type === 1 && photosForRegion.length > 0 ? (
+            <ImageCarousel
+              photos={photosForRegion as PhotosData[]}
+              activeIndex={activeIndex}
+              setActiveIndex={setActiveIndex}
+              openModal={openModal}
+              containerStyles={{ marginBottom: 0 }}
+            />
+          ) : (
+            <Image source={{ uri: photoUrl }} style={{ width: '100%', height: 220 }} />
+          )}
 
           {registrationInfo && (
             <View
@@ -985,7 +1011,57 @@ const EventScreen = ({ route }: { route: any }) => {
               )}
             </View>
 
-            {joined ? (
+            {/* TO DO */}
+            {event.settings.host_profile === +currentUserId &&
+            event.settings.type === 1 &&
+            !event.archived ? (
+              <TouchableOpacity
+                style={{
+                  flexDirection: 'row',
+                  alignItems: 'center',
+                  justifyContent: 'center',
+                  paddingVertical: 8,
+                  paddingHorizontal: 12,
+                  borderRadius: 20,
+                  backgroundColor: Colors.ORANGE,
+                  gap: 6,
+                  borderWidth: 1,
+                  borderColor: Colors.ORANGE
+                }}
+                onPress={() =>
+                  getForEditing(
+                    { token, event_id: event.id },
+                    {
+                      onSuccess: (res) => {
+                        navigation.navigate(
+                          ...([
+                            NAVIGATION_PAGES.CREATE_EVENT,
+                            {
+                              eventId: event.id,
+                              event: res
+                            }
+                          ] as never)
+                        );
+                      }
+                    }
+                  )
+                }
+              >
+                <EditSvg fill={Colors.WHITE} width={16} height={16} />
+                <Text
+                  style={{
+                    color: Colors.WHITE,
+                    fontSize: getFontSize(14),
+                    fontFamily: 'montserrat-700',
+                    textTransform: 'uppercase'
+                  }}
+                >
+                  Edit
+                </Text>
+              </TouchableOpacity>
+            ) : null}
+            {event.settings.host_profile === +currentUserId ||
+            event.archived === 1 ? null : joined ? (
               <TouchableOpacity
                 style={{
                   flexDirection: 'row',
@@ -1348,6 +1424,14 @@ const EventScreen = ({ route }: { route: any }) => {
         onClose={() => setModalInfo({ ...modalInfo, visible: false })}
         title={modalInfo.title}
       />
+      <ImageView
+        images={photosForRegion}
+        imageIndex={currentImageIndex}
+        visible={isImageModalVisible}
+        onRequestClose={() => setIsImageModalVisible(false)}
+        backgroundColor={Colors.DARK_BLUE}
+        onImageIndexChange={setActiveIndex}
+      />
     </View>
   );
 };
@@ -1365,7 +1449,7 @@ const WebDisplay = React.memo(function WebDisplay({ html }: { html: string }) {
   const processedHtml = React.useMemo(() => {
     let updatedHtml = html;
     const hrefRegex = /href="((?!http)[^"]+)"/g;
-    const imgSrcRegex = /src="((?:\.{0,2}\/)*img\/[^"]*)"/g;
+    const imgSrcRegex = /src="((?:\.{0,2}\/)*[^":]+)"/g;
 
     const normalizePath = (path: string): string => {
       const segments = path.split('/').filter(Boolean);

+ 110 - 35
src/screens/InAppScreens/TravelsScreen/EventsScreen/index.tsx

@@ -1,5 +1,13 @@
-import React, { useEffect, useState } from 'react';
-import { View, Text, Image, TouchableOpacity } from 'react-native';
+import React, { useCallback, useEffect, useRef, useState } from 'react';
+import {
+  View,
+  Text,
+  Image,
+  TouchableOpacity,
+  ScrollView,
+  LayoutAnimation,
+  findNodeHandle
+} from 'react-native';
 import { FlashList } from '@shopify/flash-list';
 import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
 import { styles } from './styles';
@@ -21,16 +29,40 @@ import moment from 'moment';
 import { API_HOST } from 'src/constants';
 import { Grayscale } from 'react-native-color-matrix-image-filters';
 import { renderSpotsText } from './utils';
-import ClockIcon from 'assets/icons/events/clock.svg';
+import ChevronIcon from 'assets/icons/chevron-left.svg';
 
 const EventsScreen = () => {
   const token = (storage.get('token', StoreType.STRING) as string) ?? null;
-  const { data, refetch } = useGetEventsListQuery(token, true);
+  const { data, refetch } = useGetEventsListQuery(token, 0, true);
+  const { data: pastData } = useGetEventsListQuery(token, 1, true);
   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 date = new Date();
+
+  const [isExpanded, setIsExpanded] = useState(false);
+  const scrollViewRef = useRef<ScrollView>(null);
+  const sectionRef = useRef<View>(null);
+
+  const toggleExpand = () => {
+    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')
+        );
+      }
+    });
+
+    setIsExpanded(!isExpanded);
+  };
 
   useEffect(() => {
     if (data && data.data) {
@@ -39,9 +71,18 @@ const EventsScreen = () => {
     }
   }, [data]);
 
-  useFocusEffect(() => {
-    refetch();
-  });
+  useEffect(() => {
+    if (pastData && pastData.data) {
+      setPastEvents(pastData.data);
+      setFilteredPastEvents(pastData.data);
+    }
+  }, [pastData]);
+
+  useFocusEffect(
+    useCallback(() => {
+      refetch();
+    }, [navigation])
+  );
 
   const handleSearch = (text: string) => {
     if (text) {
@@ -52,9 +93,19 @@ const EventsScreen = () => {
           return itemData.indexOf(textData) > -1;
         }) ?? [];
       setFilteredEvents(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);
+
       setSearchQuery(text);
     } else {
       setFilteredEvents(events);
+      setFilteredPastEvents(pastEvents);
       setSearchQuery(text);
     }
   };
@@ -85,7 +136,7 @@ const EventsScreen = () => {
     }
 
     const photo = item.photo
-      ? API_HOST + '/webapi/events/get-square-photo/' + item.id
+      ? API_HOST + '/webapi/events/get-square-photo/' + item.id + '?cacheBust=' + date
       : API_HOST + staticImgUrl;
 
     return (
@@ -95,7 +146,7 @@ const EventsScreen = () => {
             styles.card,
             item.type === 2 || item.type === 3 || item.full
               ? { backgroundColor: Colors.FILL_LIGHT }
-              : {}
+              : { backgroundColor: Colors.WHITE }
           ]}
           onPress={() =>
             navigation.navigate(...([NAVIGATION_PAGES.EVENT, { url: item.url }] as never))
@@ -132,14 +183,6 @@ const EventsScreen = () => {
                   {formatEventDate(item)}
                 </Text>
               </View>
-              {item.time && (
-                <View style={styles.row}>
-                  <ClockIcon fill={Colors.DARK_BLUE} height={14} width={14} />
-                  <Text style={[styles.dateAndLocation, { flex: 0 }]} numberOfLines={1}>
-                    {item.time}
-                  </Text>
-                </View>
-              )}
             </View>
 
             <View style={styles.row}>
@@ -192,25 +235,57 @@ const EventsScreen = () => {
         }
       />
 
-      <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}
+      <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}
         />
-      </View>
-
-      <FlashList
-        data={filteredEvents}
-        keyExtractor={(item) => item.id.toString()}
-        renderItem={renderEventCard}
-        estimatedItemSize={100}
-        contentContainerStyle={styles.listContainer}
-        showsVerticalScrollIndicator={false}
-      />
+
+        {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>
+
+              <View style={styles.chevronContainer}>
+                <ChevronIcon
+                  fill={Colors.DARK_BLUE}
+                  style={[styles.headerIcon, isExpanded ? styles.rotate : null]}
+                />
+              </View>
+            </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>
     </PageWrapper>
   );
 };

+ 45 - 1
src/screens/InAppScreens/TravelsScreen/EventsScreen/styles.tsx

@@ -100,5 +100,49 @@ export const styles = StyleSheet.create({
     fontFamily: 'montserrat-700',
     textAlign: 'center'
   },
-  row: { flexDirection: 'row', gap: 6, alignItems: 'center' }
+  row: { flexDirection: 'row', gap: 6, alignItems: 'center' },
+  sectionContainer: {
+    marginTop: 12,
+    marginBottom: 12,
+    backgroundColor: '#f9f9f9',
+    borderRadius: 8,
+    paddingHorizontal: 8
+  },
+  headerContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    flex: 1
+  },
+  chevronContainer: {
+    width: 18,
+    height: 18,
+    alignItems: 'center'
+  },
+  itemContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    marginBottom: 16,
+    justifyContent: 'space-between',
+    flex: 1
+  },
+  header: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingVertical: 12,
+    paddingHorizontal: 4,
+    justifyContent: 'space-between',
+    flex: 1
+  },
+  headerText: {
+    fontSize: 14,
+    fontWeight: 'bold',
+    color: Colors.DARK_BLUE,
+    flexShrink: 1
+  },
+  headerIcon: {
+    transform: [{ rotate: '-90deg' }]
+  },
+  rotate: {
+    transform: [{ rotate: '90deg' }]
+  }
 });

+ 8 - 2
src/types/api.ts

@@ -200,7 +200,10 @@ export enum API_ENDPOINT {
   GET_PHOTOS_FOR_REGION = 'get-photos-for-region',
   ADD_EVENT = 'add-event',
   SET_LOCATION_REGIONS = 'set-settings-regions',
-  GET_MASTER = 'get-master'
+  GET_MASTER = 'get-master',
+  GET_EVENT_FOR_EDITING = 'get-event-for-editing',
+  UPDATE_EVENT = 'update-event',
+  CANCEL_EVENT = 'cancel-event'
 }
 
 export enum API {
@@ -373,7 +376,10 @@ export enum API {
   GET_PHOTOS_FOR_REGION = `${API_ROUTE.EVENTS}/${API_ENDPOINT.GET_PHOTOS_FOR_REGION}`,
   ADD_EVENT = `${API_ROUTE.EVENTS}/${API_ENDPOINT.ADD_EVENT}`,
   SET_LOCATION_REGIONS = `${API_ROUTE.LOCATION}/${API_ENDPOINT.SET_LOCATION_REGIONS}`,
-  GET_MASTER = `${API_ROUTE.RANKING}/${API_ENDPOINT.GET_MASTER}`
+  GET_MASTER = `${API_ROUTE.RANKING}/${API_ENDPOINT.GET_MASTER}`,
+  GET_EVENT_FOR_EDITING = `${API_ROUTE.EVENTS}/${API_ENDPOINT.GET_EVENT_FOR_EDITING}`,
+  UPDATE_EVENT = `${API_ROUTE.EVENTS}/${API_ENDPOINT.UPDATE_EVENT}`,
+  CANCEL_EVENT = `${API_ROUTE.EVENTS}/${API_ENDPOINT.CANCEL_EVENT}`
 }
 
 export type BaseAxiosError = AxiosError;