Pārlūkot izejas kodu

navigation for events, attachments fixes, location sending, deep link for profile

Viktoriia 5 mēneši atpakaļ
vecāks
revīzija
a45783c6f4

+ 11 - 1
App.tsx

@@ -15,7 +15,7 @@ import { setupInterceptors } from 'src/utils/request';
 import { ErrorModal, WarningModal } from 'src/components';
 import React from 'react';
 import { Linking, Platform } from 'react-native';
-import { API_URL, APP_VERSION } from 'src/constants';
+import { API_HOST, API_URL, APP_VERSION } from 'src/constants';
 import axios from 'axios';
 import { API } from 'src/types';
 
@@ -34,6 +34,15 @@ Sentry.init({
   ignoreErrors: ['Network Error', 'ECONNABORTED', 'timeout of 10000ms exceeded']
 });
 
+const linking = {
+  prefixes: [API_HOST, 'nomadmania://'],
+  config: {
+    screens: {
+      publicProfileView: 'profile/:userId'
+    }
+  }
+};
+
 const App = () => {
   return (
     <QueryClientProvider client={queryClient}>
@@ -95,6 +104,7 @@ const InnerApp = () => {
           onReady={() => {
             routingInstrumentation.registerNavigationContainer(navigation);
           }}
+          linking={linking}
         >
           <Route />
           <ConnectionBanner />

+ 56 - 45
Route.tsx

@@ -94,6 +94,8 @@ import { Splash } from 'src/components/SplashSpinner';
 import { useMessagesStore } from 'src/stores/unreadMessagesStore';
 import LocationSharingScreen from 'src/screens/LocationSharingScreen';
 import { useFriendsNotificationsStore } from 'src/stores/friendsNotificationsStore';
+import EventsScreen from 'src/screens/InAppScreens/TravelsScreen/EventsScreen';
+import { NavigationProvider } from 'src/contexts/NavigationContext';
 
 enableScreens();
 
@@ -304,6 +306,7 @@ const Route = () => {
             <ScreenStack.Screen name={NAVIGATION_PAGES.DARE} component={DareScreen} />
             <ScreenStack.Screen name={NAVIGATION_PAGES.FIXERS} component={FixersScreen} />
             <ScreenStack.Screen name={NAVIGATION_PAGES.ADD_FIXER} component={AddNewFixerScreen} />
+            <ScreenStack.Screen name={NAVIGATION_PAGES.EVENTS} component={EventsScreen} />
             <ScreenStack.Screen
               name={NAVIGATION_PAGES.FIXERS_COMMENTS}
               component={FixersCommentsScreen}
@@ -463,51 +466,59 @@ const Route = () => {
 
   return (
     <PushNotificationProvider>
-      <ScreenStack.Navigator
-        screenOptions={{ headerShown: false, cardStyle: { backgroundColor: 'white' } }}
-        initialRouteName={token ? NAVIGATION_PAGES.IN_APP : NAVIGATION_PAGES.WELCOME}
-      >
-        <ScreenStack.Screen name={NAVIGATION_PAGES.WELCOME} component={WelcomeScreen} />
-        <ScreenStack.Screen name={NAVIGATION_PAGES.LOGIN} component={LoginScreen} />
-        <ScreenStack.Screen name={NAVIGATION_PAGES.REGISTER} component={JoinUsScreen} />
-        <ScreenStack.Screen name={NAVIGATION_PAGES.REGISTER_ACCOUNT_DATA} component={EditAccount} />
-        <ScreenStack.Screen
-          name={NAVIGATION_PAGES.RESET_PASSWORD}
-          component={ResetPasswordScreen}
-        />
-        <ScreenStack.Screen
-          name={NAVIGATION_PAGES.RESET_PASSWORD_DEEP}
-          component={ResetPasswordDeepScreen}
-        />
-        <ScreenStack.Screen name={NAVIGATION_PAGES.INFO} component={InfoScreen} />
-        <ScreenStack.Screen name={NAVIGATION_PAGES.JOIN_INFO} component={JoinInfoScreen} />
-        <ScreenStack.Screen name={NAVIGATION_PAGES.DISCOVER_INFO} component={DiscoverInfoScreen} />
-        <ScreenStack.Screen name={NAVIGATION_PAGES.PLAN_INFO} component={PlanInfoScreen} />
-        <ScreenStack.Screen
-          name={NAVIGATION_PAGES.FIRST_STEPS_INFO}
-          component={FirstStepsInfoScreen}
-        />
-        <ScreenStack.Screen
-          name={NAVIGATION_PAGES.COUNTRIES_INFO}
-          component={CountriesInfoScreen}
-        />
-        <ScreenStack.Screen name={NAVIGATION_PAGES.DARE_INFO} component={DareInfoScreen} />
-        <ScreenStack.Screen name={NAVIGATION_PAGES.REGIONS_INFO} component={RegionsInfoScreen} />
-        <ScreenStack.Screen name={NAVIGATION_PAGES.TRIPS_INFO} component={TripsInfoScreen} />
-        <ScreenStack.Screen name={NAVIGATION_PAGES.FIXERS_INFO} component={FixersInfoScreen} />
-        <ScreenStack.Screen name={NAVIGATION_PAGES.EARTH_INFO} component={EarthInfoScreen} />
-        <ScreenStack.Screen name={NAVIGATION_PAGES.IN_APP}>
-          {() => (
-            <MapDrawer.Navigator drawerContent={(props) => <MenuDrawer {...props} />}>
-              <MapDrawer.Screen
-                name="DrawerApp"
-                component={BottomTabNavigator}
-                options={{ headerShown: false }}
-              />
-            </MapDrawer.Navigator>
-          )}
-        </ScreenStack.Screen>
-      </ScreenStack.Navigator>
+      <NavigationProvider>
+        <ScreenStack.Navigator
+          screenOptions={{ headerShown: false, cardStyle: { backgroundColor: 'white' } }}
+          initialRouteName={token ? NAVIGATION_PAGES.IN_APP : NAVIGATION_PAGES.WELCOME}
+        >
+          <ScreenStack.Screen name={NAVIGATION_PAGES.WELCOME} component={WelcomeScreen} />
+          <ScreenStack.Screen name={NAVIGATION_PAGES.LOGIN} component={LoginScreen} />
+          <ScreenStack.Screen name={NAVIGATION_PAGES.REGISTER} component={JoinUsScreen} />
+          <ScreenStack.Screen
+            name={NAVIGATION_PAGES.REGISTER_ACCOUNT_DATA}
+            component={EditAccount}
+          />
+          <ScreenStack.Screen
+            name={NAVIGATION_PAGES.RESET_PASSWORD}
+            component={ResetPasswordScreen}
+          />
+          <ScreenStack.Screen
+            name={NAVIGATION_PAGES.RESET_PASSWORD_DEEP}
+            component={ResetPasswordDeepScreen}
+          />
+          <ScreenStack.Screen name={NAVIGATION_PAGES.INFO} component={InfoScreen} />
+          <ScreenStack.Screen name={NAVIGATION_PAGES.JOIN_INFO} component={JoinInfoScreen} />
+          <ScreenStack.Screen
+            name={NAVIGATION_PAGES.DISCOVER_INFO}
+            component={DiscoverInfoScreen}
+          />
+          <ScreenStack.Screen name={NAVIGATION_PAGES.PLAN_INFO} component={PlanInfoScreen} />
+          <ScreenStack.Screen
+            name={NAVIGATION_PAGES.FIRST_STEPS_INFO}
+            component={FirstStepsInfoScreen}
+          />
+          <ScreenStack.Screen
+            name={NAVIGATION_PAGES.COUNTRIES_INFO}
+            component={CountriesInfoScreen}
+          />
+          <ScreenStack.Screen name={NAVIGATION_PAGES.DARE_INFO} component={DareInfoScreen} />
+          <ScreenStack.Screen name={NAVIGATION_PAGES.REGIONS_INFO} component={RegionsInfoScreen} />
+          <ScreenStack.Screen name={NAVIGATION_PAGES.TRIPS_INFO} component={TripsInfoScreen} />
+          <ScreenStack.Screen name={NAVIGATION_PAGES.FIXERS_INFO} component={FixersInfoScreen} />
+          <ScreenStack.Screen name={NAVIGATION_PAGES.EARTH_INFO} component={EarthInfoScreen} />
+          <ScreenStack.Screen name={NAVIGATION_PAGES.IN_APP}>
+            {() => (
+              <MapDrawer.Navigator drawerContent={(props) => <MenuDrawer {...props} />}>
+                <MapDrawer.Screen
+                  name="DrawerApp"
+                  component={BottomTabNavigator}
+                  options={{ headerShown: false }}
+                />
+              </MapDrawer.Navigator>
+            )}
+          </ScreenStack.Screen>
+        </ScreenStack.Navigator>
+      </NavigationProvider>
     </PushNotificationProvider>
   );
 };

+ 14 - 0
app.config.ts

@@ -69,6 +69,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
     config: {
       googleMapsApiKey: env.IOS_GOOGLE_MAP_APIKEY
     },
+    associatedDomains: ['applinks:nomadmania.com'],
     infoPlist: {
       UIBackgroundModes: ['fetch'],
       NSLocationAlwaysUsageDescription:
@@ -101,6 +102,19 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
         apiKey: env.ANDROID_GOOGLE_MAP_APIKEY
       }
     },
+    intentFilters: [
+      {
+        action: 'VIEW',
+        data: [
+          {
+            scheme: 'https',
+            host: 'nomadmania.com',
+            pathPrefix: '/profile/'
+          }
+        ],
+        category: ['BROWSABLE', 'DEFAULT']
+      }
+    ],
     googleServicesFile: './google-services.json',
     permissions: [
       // 'ACCESS_BACKGROUND_LOCATION',

+ 10 - 0
assets/icons/events/calendar-solid.svg

@@ -0,0 +1,10 @@
+<svg width="29" height="32" viewBox="0 0 29 32" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_4524_47052)">
+<path d="M8.25 0C9.35625 0 10.25 0.89375 10.25 2V4H18.25V2C18.25 0.89375 19.1437 0 20.25 0C21.3563 0 22.25 0.89375 22.25 2V4H25.25C26.9062 4 28.25 5.34375 28.25 7V10H0.25V7C0.25 5.34375 1.59375 4 3.25 4H6.25V2C6.25 0.89375 7.14375 0 8.25 0ZM0.25 12H28.25V29C28.25 30.6562 26.9062 32 25.25 32H3.25C1.59375 32 0.25 30.6562 0.25 29V12ZM4.25 17V19C4.25 19.55 4.7 20 5.25 20H7.25C7.8 20 8.25 19.55 8.25 19V17C8.25 16.45 7.8 16 7.25 16H5.25C4.7 16 4.25 16.45 4.25 17ZM12.25 17V19C12.25 19.55 12.7 20 13.25 20H15.25C15.8 20 16.25 19.55 16.25 19V17C16.25 16.45 15.8 16 15.25 16H13.25C12.7 16 12.25 16.45 12.25 17ZM21.25 16C20.7 16 20.25 16.45 20.25 17V19C20.25 19.55 20.7 20 21.25 20H23.25C23.8 20 24.25 19.55 24.25 19V17C24.25 16.45 23.8 16 23.25 16H21.25ZM4.25 25V27C4.25 27.55 4.7 28 5.25 28H7.25C7.8 28 8.25 27.55 8.25 27V25C8.25 24.45 7.8 24 7.25 24H5.25C4.7 24 4.25 24.45 4.25 25ZM13.25 24C12.7 24 12.25 24.45 12.25 25V27C12.25 27.55 12.7 28 13.25 28H15.25C15.8 28 16.25 27.55 16.25 27V25C16.25 24.45 15.8 24 15.25 24H13.25ZM20.25 25V27C20.25 27.55 20.7 28 21.25 28H23.25C23.8 28 24.25 27.55 24.25 27V25C24.25 24.45 23.8 24 23.25 24H21.25C20.7 24 20.25 24.45 20.25 25Z" fill="#ED9334"/>
+</g>
+<defs>
+<clipPath id="clip0_4524_47052">
+<rect width="28" height="32" fill="white" transform="translate(0.25)"/>
+</clipPath>
+</defs>
+</svg>

+ 1 - 0
package.json

@@ -87,6 +87,7 @@
     "react-native-share": "^10.2.1",
     "react-native-svg": "15.2.0",
     "react-native-tab-view": "^3.5.2",
+    "react-native-url-polyfill": "^2.0.0",
     "react-native-video": "^6.5.0",
     "react-native-view-shot": "^3.7.0",
     "react-native-walkthrough-tooltip": "^1.6.0",

+ 69 - 0
src/contexts/NavigationContext.tsx

@@ -0,0 +1,69 @@
+import React, { createContext, useContext, useEffect, useState } from 'react';
+import { Linking, Platform } from 'react-native';
+import { useNavigation } from '@react-navigation/native';
+import { NAVIGATION_PAGES } from 'src/types';
+import { storage, StoreType } from 'src/storage';
+
+interface NavigationContextType {
+  handleDeepLink: () => Promise<void>;
+}
+
+const NavigationContext = createContext<NavigationContextType | null>(null);
+
+const parseURL = (url: string) => {
+  const parsedUrl = new URL(url);
+  const path = parsedUrl.pathname;
+  const queryParams = Object.fromEntries(parsedUrl.searchParams.entries());
+  return { path, queryParams };
+};
+
+export const useNavigationContext = () => {
+  const context = useContext(NavigationContext);
+  if (!context) {
+    throw new Error('useNavigationContext must be used within a NavigationProvider');
+  }
+  return context;
+};
+
+export const NavigationProvider = ({ children }: { children: React.ReactNode }) => {
+  const navigation = useNavigation();
+  const token = storage.get('token', StoreType.STRING);
+  const [initialUrlProcessed, setInitialUrlProcessed] = useState(false);
+
+  const handleDeepLink = async (url?: string) => {
+    const link = url || (await Linking.getInitialURL());
+    console.log('Deep Link URL:', link);
+    if (link) {
+      const { path } = parseURL(link);
+      console.log('Parsed URL:', { path });
+      if (path.startsWith('/profile') && token) {
+        const segments = path.split('/');
+        const userId = segments[2];
+        console.log('Navigating to public profile:', userId);
+        navigation.navigate(...([NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW, { userId }] as never));
+      }
+    }
+    if (!initialUrlProcessed) {
+      setInitialUrlProcessed(true);
+    }
+  };
+
+  useEffect(() => {
+    if (!initialUrlProcessed) {
+      handleDeepLink();
+    }
+
+    const subscription = Linking.addEventListener('url', (event) => {
+      console.log('Linking event:', event);
+      handleDeepLink(event.url);
+    });
+
+    return () => {
+      subscription.remove();
+    };
+  }, [initialUrlProcessed]);
+
+  return (
+    <NavigationContext.Provider value={{ handleDeepLink }}>{children}</NavigationContext.Provider>
+  );
+};

+ 73 - 77
src/screens/InAppScreens/MessagesScreen/Components/AttachmentsModal.tsx

@@ -1,6 +1,6 @@
 import React, { useCallback, useState } from 'react';
-import { StyleSheet, TouchableOpacity, View, Text } from 'react-native';
-import ActionSheet, { SheetManager } from 'react-native-actions-sheet';
+import { StyleSheet, TouchableOpacity, View, Text, Button } from 'react-native';
+import ActionSheet, { Route, SheetManager, useSheetRouter } from 'react-native-actions-sheet';
 import { getFontSize } from 'src/utils';
 import { Colors } from 'src/theme';
 import { WarningProps } from '../types';
@@ -11,6 +11,7 @@ import * as DocumentPicker from 'react-native-document-picker';
 
 import MegaphoneIcon from 'assets/icons/messages/megaphone.svg';
 import { MaterialCommunityIcons } from '@expo/vector-icons';
+import RouteB from './RouteB';
 
 const AttachmentsModal = () => {
   const insets = useSafeAreaInsets();
@@ -18,6 +19,18 @@ const AttachmentsModal = () => {
   const [shouldOpenWarningModal, setShouldOpenWarningModal] = useState<WarningProps | null>(null);
   const { mutateAsync: reportUser } = usePostReportConversationMutation();
 
+  const router = useSheetRouter('sheet-router');
+
+  const [currentLocation, setCurrentLocation] = useState<{
+    latitude: number;
+    longitude: number;
+  } | null>(null);
+  const [selectedLocation, setSelectedLocation] = useState<{
+    latitude: number;
+    longitude: number;
+  } | null>(null);
+  const [isLoading, setIsLoading] = useState(true);
+
   const handleSheetOpen = (payload: any) => {
     setChatData(payload);
   };
@@ -99,24 +112,6 @@ const AttachmentsModal = () => {
     }
   }, [chatData?.onSendMedia, chatData?.closeOptions]);
 
-  const handleShareLocation = useCallback(async () => {
-    if (!chatData) return;
-    try {
-      // TODO:
-      // const { status } = await Location.requestForegroundPermissionsAsync();
-      // if (status !== 'granted') {}
-
-      // const loc = await Location.getCurrentPositionAsync({});
-      // const coords = { latitude: loc.coords.latitude, longitude: loc.coords.longitude };
-
-      const coords = { latitude: 50.4501, longitude: 30.5234 };
-      chatData.onSendLocation(coords);
-      SheetManager.hide('chat-attachments');
-    } catch (err) {
-      console.warn('Location error: ', err);
-    }
-  }, [chatData?.onSendLocation, chatData?.closeOptions]);
-
   const handleShareLiveLocation = useCallback(() => {
     if (!chatData) return;
     chatData.onShareLiveLocation();
@@ -152,62 +147,15 @@ const AttachmentsModal = () => {
     SheetManager.hide('chat-attachments');
   }, [chatData?.onSendFile, chatData?.closeOptions]);
 
-  return (
-    <ActionSheet
-      id="chat-attachments"
-      gestureEnabled={true}
-      containerStyle={{
-        borderTopLeftRadius: 15,
-        borderTopRightRadius: 15
-      }}
-      defaultOverlayOpacity={0.3}
-      onBeforeShow={(sheetRef) => {
-        const payload = sheetRef || null;
-        handleSheetOpen(payload);
-      }}
-      onClose={() => {
-        if (shouldOpenWarningModal) {
-          chatData?.setModalInfo({
-            visible: true,
-            type: 'delete',
-            title: shouldOpenWarningModal.title,
-            buttonTitle: shouldOpenWarningModal.buttonTitle,
-            message: shouldOpenWarningModal.message,
-            action: shouldOpenWarningModal.action
-          });
-        }
-      }}
-    >
-      {/* <View
-        style={{
-          backgroundColor: 'white',
-          paddingHorizontal: 16,
-          gap: 16,
-          paddingTop: 8,
-          paddingBottom: 8 + insets.bottom
-        }}
+  const RouteA = () => {
+    const router = useSheetRouter('sheet-router');
+    return (
+      <View
+        style={[
+          styles.container,
+          { paddingBottom: 8 + insets.bottom, backgroundColor: Colors.FILL_LIGHT }
+        ]}
       >
-        <TouchableOpacity style={[styles.option]} onPress={handleReport}>
-          <Text style={[styles.optionText]}>Gallery</Text>
-          <MegaphoneIcon fill={Colors.DARK_BLUE} />
-        </TouchableOpacity>
-
-        <TouchableOpacity style={[styles.option]} onPress={handleReport}>
-          <Text style={[styles.optionText]}>Camera</Text>
-          <MegaphoneIcon fill={Colors.DARK_BLUE} />
-        </TouchableOpacity>
-
-        <TouchableOpacity style={[styles.option]} onPress={handleReport}>
-          <Text style={[styles.optionText]}>Location</Text>
-          <MegaphoneIcon fill={Colors.DARK_BLUE} />
-        </TouchableOpacity>
-
-        <TouchableOpacity style={[styles.option, styles.dangerOption]} onPress={handleReport}>
-          <Text style={[styles.optionText, styles.dangerText]}>Report {chatData?.name}</Text>
-          <MegaphoneIcon fill={Colors.RED} />
-        </TouchableOpacity>
-      </View> */}
-      <View style={[styles.container, { paddingBottom: 8 + insets.bottom }]}>
         <View style={styles.optionRow}>
           <TouchableOpacity style={styles.optionItem} onPress={handleOpenGallery}>
             <MaterialCommunityIcons name="image" size={36} color={Colors.ORANGE} />
@@ -219,7 +167,12 @@ const AttachmentsModal = () => {
             <Text style={styles.optionLabel}>Camera</Text>
           </TouchableOpacity>
 
-          <TouchableOpacity style={styles.optionItem} onPress={handleShareLocation}>
+          <TouchableOpacity
+            style={styles.optionItem}
+            onPress={() => {
+              router?.navigate('route-b');
+            }}
+          >
             <MaterialCommunityIcons name="map-marker" size={36} color={Colors.ORANGE} />
             <Text style={styles.optionLabel}>Location</Text>
           </TouchableOpacity>
@@ -241,7 +194,50 @@ const AttachmentsModal = () => {
           <View style={styles.optionItem}></View>
         </View>
       </View>
-    </ActionSheet>
+    );
+  };
+
+  const routes: Route[] = [
+    {
+      name: 'route-a',
+      component: RouteA
+    },
+    {
+      name: 'route-b',
+      component: RouteB,
+      params: { onSendLocation: chatData?.onSendLocation, insetsBottom: insets.bottom }
+    }
+  ];
+
+  return (
+    <ActionSheet
+      id="chat-attachments"
+      gestureEnabled={true}
+      containerStyle={{
+        backgroundColor: Colors.FILL_LIGHT
+      }}
+      enableRouterBackNavigation={true}
+      routes={routes}
+      initialRoute="route-a"
+      defaultOverlayOpacity={0}
+      indicatorStyle={{ backgroundColor: Colors.WHITE }}
+      onBeforeShow={(sheetRef) => {
+        const payload = sheetRef || null;
+        handleSheetOpen(payload);
+      }}
+      onClose={() => {
+        if (shouldOpenWarningModal) {
+          chatData?.setModalInfo({
+            visible: true,
+            type: 'delete',
+            title: shouldOpenWarningModal.title,
+            buttonTitle: shouldOpenWarningModal.buttonTitle,
+            message: shouldOpenWarningModal.message,
+            action: shouldOpenWarningModal.action
+          });
+        }
+      }}
+    />
   );
 };
 

+ 194 - 0
src/screens/InAppScreens/MessagesScreen/Components/RouteB.tsx

@@ -0,0 +1,194 @@
+import React, { useEffect, useRef, useState } from 'react';
+import { StyleSheet, View, Text, ActivityIndicator, TouchableOpacity } from 'react-native';
+import * as Location from 'expo-location';
+import * as MapLibreRN from '@maplibre/maplibre-react-native';
+import { SheetManager, useSheetRouteParams, useSheetRouter } from 'react-native-actions-sheet';
+import { Colors } from 'src/theme';
+import { VECTOR_MAP_HOST } from 'src/constants';
+import { ButtonVariants } from 'src/types/components';
+import { Button } from 'src/components';
+
+const RouteB = () => {
+  const router = useSheetRouter('sheet-router');
+  const params = useSheetRouteParams('sheet-router', 'route-b');
+  const {
+    onSendLocation,
+    insetsBottom
+  }: {
+    onSendLocation: (coords: { latitude: number; longitude: number }) => void;
+    insetsBottom: number;
+  } = params;
+
+  const [currentLocation, setCurrentLocation] = useState<{
+    latitude: number;
+    longitude: number;
+  } | null>(null);
+  const [selectedLocation, setSelectedLocation] = useState<{
+    latitude: number;
+    longitude: number;
+  } | null>(null);
+  const [isLoading, setIsLoading] = useState(true);
+  const cameraRef = useRef<MapLibreRN.CameraRef | null>(null);
+
+  useEffect(() => {
+    const getLocation = async () => {
+      try {
+        const { status } = await Location.requestForegroundPermissionsAsync();
+        if (status !== 'granted') {
+          console.warn('Permission for location not granted');
+          setIsLoading(false);
+          return;
+        }
+
+        const location = await Location.getCurrentPositionAsync({});
+        const coords = {
+          latitude: location.coords.latitude,
+          longitude: location.coords.longitude
+        };
+        setCurrentLocation(coords);
+        setSelectedLocation(coords);
+      } catch (err) {
+        console.warn('Error fetching location:', err);
+      } finally {
+        setIsLoading(false);
+      }
+    };
+
+    getLocation();
+  }, []);
+
+  useEffect(() => {
+    if (currentLocation) {
+      const timeoutId = setTimeout(() => {
+        if (cameraRef.current) {
+          cameraRef.current.setCamera({
+            centerCoordinate: [currentLocation.longitude, currentLocation.latitude],
+            zoomLevel: 14,
+            animationDuration: 500
+          });
+        } else {
+          console.warn('Camera ref is not available.');
+        }
+      }, 500);
+
+      return () => clearTimeout(timeoutId);
+    }
+  }, [currentLocation]);
+
+  const confirmLocation = () => {
+    if (selectedLocation) {
+      onSendLocation(selectedLocation);
+    }
+    SheetManager.hide('location-picker');
+    SheetManager.hide('chat-attachments');
+  };
+
+  const sendCurrentLocation = () => {
+    if (currentLocation) {
+      onSendLocation(currentLocation);
+    }
+    SheetManager.hide('location-picker');
+    SheetManager.hide('chat-attachments');
+  };
+
+  const handleRegionChange = (event: any) => {
+    const { geometry } = event;
+    if (geometry) {
+      const [longitude, latitude] = geometry.coordinates;
+      setSelectedLocation({ latitude, longitude });
+    }
+  };
+
+  if (isLoading) {
+    return (
+      <View style={styles.loadingContainer}>
+        <ActivityIndicator size="large" color={Colors.DARK_BLUE} />
+        <Text>Loading your location...</Text>
+      </View>
+    );
+  }
+
+  return (
+    <View style={[styles.container, { paddingBottom: 8 + insetsBottom }]}>
+      <MapLibreRN.MapView
+        style={{ flex: 1 }}
+        mapStyle={VECTOR_MAP_HOST + '/nomadmania-maps.json'}
+        compassEnabled={false}
+        rotateEnabled={false}
+        attributionEnabled={false}
+        onRegionDidChange={handleRegionChange}
+      >
+        <MapLibreRN.Camera ref={cameraRef} />
+        {currentLocation && (
+          <MapLibreRN.PointAnnotation
+            id="currentLocation"
+            coordinate={[currentLocation.longitude, currentLocation.latitude]}
+          >
+            <View style={styles.currentLocationMarker} />
+          </MapLibreRN.PointAnnotation>
+        )}
+
+        <View style={styles.centerMarker}>
+          <View style={styles.selectedLocationMarker} />
+        </View>
+      </MapLibreRN.MapView>
+
+      <View style={styles.mapActions}>
+        <Button children="Send my location" onPress={sendCurrentLocation} />
+        <Button children="Confirm" onPress={confirmLocation} />
+        <Button
+          children="Close"
+          onPress={() => router?.goBack()}
+          variant={ButtonVariants.OPACITY}
+          containerStyles={{ backgroundColor: Colors.WHITE, borderColor: '#B7C6CB' }}
+          textStyles={{ color: Colors.DARK_BLUE }}
+        />
+      </View>
+    </View>
+  );
+};
+
+const styles = StyleSheet.create({
+  container: {
+    backgroundColor: Colors.FILL_LIGHT,
+    minHeight: 600,
+    gap: 16
+  },
+  mapActions: {
+    flexDirection: 'column',
+    gap: 8,
+    paddingHorizontal: 16
+  },
+  currentLocationMarker: {
+    width: 20,
+    height: 20,
+    backgroundColor: Colors.ORANGE,
+    borderRadius: 10,
+    borderWidth: 2,
+    borderColor: Colors.WHITE
+  },
+  selectedLocationMarker: {
+    width: 24,
+    height: 24,
+    backgroundColor: Colors.ORANGE,
+    borderRadius: 12,
+    borderWidth: 2,
+    borderColor: Colors.WHITE
+  },
+  centerMarker: {
+    position: 'absolute',
+    top: '50%',
+    left: '50%',
+    marginLeft: -12,
+    marginTop: -12,
+    zIndex: 1000
+  },
+  loadingContainer: {
+    flex: 1,
+    justifyContent: 'center',
+    alignItems: 'center',
+    backgroundColor: Colors.FILL_LIGHT
+  }
+});
+
+export default RouteB;

+ 237 - 0
src/screens/InAppScreens/TravelsScreen/EventsScreen/index.tsx

@@ -0,0 +1,237 @@
+import React, { FC, useCallback, useEffect, useState } from 'react';
+import { View, Text, Image, TouchableOpacity, Platform } from 'react-native';
+import ImageView from 'better-react-native-image-viewing';
+import { styles } from './styles';
+import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
+import { Colors } from 'src/theme';
+import { styles as ButtonStyles } from 'src/components/RegionPopup/style';
+
+import { ScrollView } from 'react-native-gesture-handler';
+import { NAVIGATION_PAGES } from 'src/types';
+import { API_HOST } from 'src/constants';
+import { StoreType, storage } from 'src/storage';
+import { ButtonVariants } from 'src/types/components';
+import formatNumber from '../../TravelsScreen/utils/formatNumber';
+
+import ChevronLeft from 'assets/icons/chevron-left.svg';
+import MapSvg from 'assets/icons/travels-screens/map-location.svg';
+import AddImgSvg from 'assets/icons/travels-screens/add-img.svg';
+import ShareIcon from 'assets/icons/share.svg';
+
+const EventsScreen = () => {
+  const token = (storage.get('token', StoreType.STRING) as string) ?? null;
+  const navigation = useNavigation();
+  const [isLoading, setIsLoading] = useState(true);
+
+  return (
+    <View style={styles.container}>
+      <TouchableOpacity
+        onPress={() => {
+          navigation.goBack();
+        }}
+        style={styles.backButton}
+      >
+        <View style={styles.chevronWrapper}>
+          <ChevronLeft fill={Colors.WHITE} />
+        </View>
+      </TouchableOpacity>
+
+      <ScrollView
+        contentContainerStyle={{}}
+        nestedScrollEnabled={true}
+        showsVerticalScrollIndicator={false}
+      >
+        <View style={styles.emptyImage}>
+          <Image
+            source={require('../../../../../assets/images/logo-opacity.png')}
+            style={{ width: 100, height: 100 }}
+          />
+          <Text style={styles.emptyImageText}>No image available at this location</Text>
+        </View>
+        <TouchableOpacity onPress={() => {}} style={styles.addPhotoButton}>
+          <View style={styles.chevronWrapper}>
+            <AddImgSvg fill={Colors.WHITE} width={20} height={20} />
+          </View>
+        </TouchableOpacity>
+
+        <TouchableOpacity
+          onPress={() => {
+            // route.params?.isTravelsScreen || route.params?.isProfileScreen
+            //   ? 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' }
+            //                 }
+            //               ]
+            //             }
+            //           }
+            //         ]
+            //       })
+            //     )
+            //   : navigation.goBack();
+          }}
+          style={styles.goToMapBtn}
+        >
+          <View style={styles.chevronWrapper}>
+            <MapSvg fill={Colors.WHITE} />
+          </View>
+        </TouchableOpacity>
+
+        <View style={styles.wrapper}>
+          <View style={styles.nameContainer}>
+            <Text style={styles.title}>Meeting in San Clemente</Text>
+            <View style={[ButtonStyles.btnContainer, { gap: 2, flex: 0 }]}>
+              <TouchableOpacity
+                style={[ButtonStyles.btn, ButtonStyles.markVisitedButton]}
+                onPress={() => {}}
+              >
+                <Text style={[ButtonStyles.markVisitedText, { fontFamily: 'redhat-700' }]}>
+                  JOIN
+                </Text>
+              </TouchableOpacity>
+
+              <TouchableOpacity
+                onPress={() => {}}
+                style={{
+                  alignItems: 'center',
+                  justifyContent: 'center',
+                  padding: 8,
+                  paddingRight: 0
+                }}
+              >
+                <ShareIcon
+                  width={20}
+                  height={20}
+                  fill={Colors.DARK_BLUE}
+                  style={{ alignSelf: 'center' }}
+                />
+              </TouchableOpacity>
+            </View>
+          </View>
+
+          <View style={styles.divider} />
+
+          <View style={styles.stats}>
+            {true ? (
+              <View style={{ gap: 8, flex: 1 }}>
+                <Text style={[styles.travelSeriesTitle, { fontSize: 12 }]}>Host</Text>
+                <TouchableOpacity
+                  style={[styles.statItem, { justifyContent: 'flex-start' }]}
+                  onPress={() =>
+                    navigation.navigate(
+                      ...([
+                        NAVIGATION_PAGES.USERS_LIST,
+                        {
+                          id: 720,
+                          isFromHere: true,
+                          type: 'nm'
+                        }
+                      ] as never)
+                    )
+                  }
+                >
+                  <View style={styles.userImageContainer}>
+                    <Image
+                      source={{ uri: API_HOST }}
+                      style={[styles.userImage, { marginLeft: 0 }]}
+                    />
+                    <View style={{ justifyContent: 'space-between' }}>
+                      <Text
+                        style={{
+                          fontFamily: 'montserrat-700',
+                          fontSize: 12,
+                          color: Colors.DARK_BLUE
+                        }}
+                      >
+                        Harry Mitsidis
+                      </Text>
+                      <Text style={{ fontWeight: '600', fontSize: 12, color: Colors.DARK_BLUE }}>
+                        NM: <Text style={{ fontFamily: 'montserrat-700' }}>1284</Text> / UN:{' '}
+                        <Text style={{ fontFamily: 'montserrat-700' }}>193</Text>
+                      </Text>
+                    </View>
+                  </View>
+                </TouchableOpacity>
+              </View>
+            ) : (
+              <View style={[styles.statItem, { justifyContent: 'flex-start' }]} />
+            )}
+
+            {true ? (
+              <View style={{ gap: 8, flex: 1 }}>
+                <Text style={[styles.travelSeriesTitle, { fontSize: 12 }]}>Participants</Text>
+
+                <TouchableOpacity
+                  style={[styles.statItem, { justifyContent: 'flex-end' }]}
+                  onPress={() =>
+                    navigation.navigate(
+                      ...([
+                        NAVIGATION_PAGES.USERS_LIST,
+                        {
+                          id: 720,
+                          isFromHere: false,
+                          type: 'nm'
+                        }
+                      ] as never)
+                    )
+                  }
+                >
+                  <View style={styles.userImageContainer}>
+                    {/* {data?.data.users_who_visited_region &&
+                    data?.data.users_who_visited_region.length > 0 &&
+                    data?.data.users_who_visited_region?.map((user, index) => (
+                      <Image
+                        key={index}
+                        source={{ uri: API_HOST + user }}
+                        style={[styles.userImage]}
+                      />
+                    ))} */}
+                    <View style={styles.userCountContainer}>
+                      <Text style={styles.userCount}>
+                        {/* {formatNumber(data?.data.users_who_visited_region_count ?? 0)} */}
+                      </Text>
+                    </View>
+                  </View>
+                </TouchableOpacity>
+              </View>
+            ) : (
+              <View style={[styles.statItem, { justifyContent: 'flex-end' }]} />
+            )}
+          </View>
+
+          <View style={[styles.divider]} />
+
+          <View style={{ gap: 16 }}>
+            <Text style={styles.travelSeriesTitle}>Details</Text>
+            <Text style={{ fontWeight: '600', fontSize: 13, color: Colors.DARK_BLUE }}>
+              Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum
+              has been the industry's standard dummy text ever since the 1500s, when an unknown
+              printer took a galley of type and scrambled it to make a type specimen book. It has
+              survived not only five centuries, but also the leap into electronic typesetting,
+              remaining essentially unchanged. It was popularised in the 1960s with the release of
+              Letraset sheets containing Lorem Ipsum passages, and more recently with desktop
+              publishing software like Aldus PageMaker including versions of Lorem Ipsum.
+            </Text>
+          </View>
+
+          <View style={{ gap: 16 }}>
+            <Text style={styles.travelSeriesTitle}>Photos</Text>
+          </View>
+
+          <View style={{ gap: 16 }}>
+            <Text style={styles.travelSeriesTitle}>Attachments</Text>
+          </View>
+        </View>
+      </ScrollView>
+    </View>
+  );
+};
+
+export default EventsScreen;

+ 216 - 0
src/screens/InAppScreens/TravelsScreen/EventsScreen/styles.tsx

@@ -0,0 +1,216 @@
+import { StyleSheet } from 'react-native';
+import { Colors } from '../../../../theme';
+import { getFontSize } from 'src/utils';
+
+export const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    backgroundColor: 'white',
+    position: 'relative'
+  },
+  chevronWrapper: {
+    width: 42,
+    height: 42,
+    borderRadius: 21,
+    justifyContent: 'center',
+    alignItems: 'center',
+    backgroundColor: 'rgba(0, 0, 0, 0.3)'
+  },
+  backButton: {
+    position: 'absolute',
+    width: 50,
+    height: 50,
+    top: 50,
+    left: 10,
+    justifyContent: 'center',
+    alignItems: 'center',
+    zIndex: 2
+  },
+  emptyImage: {
+    height: 220,
+    width: '100%',
+    marginBottom: 12,
+    backgroundColor: Colors.FILL_LIGHT,
+    alignItems: 'center',
+    justifyContent: 'center',
+    gap: 16,
+    paddingTop: 24,
+    shadowColor: '#00000026',
+    shadowOffset: { width: 0, height: 0 },
+    shadowOpacity: 0.15,
+    shadowRadius: 8,
+    elevation: 8
+  },
+  emptyImageText: { fontWeight: '600', color: '#808080', fontSize: 12 },
+  imageFooter: { paddingBottom: 50, paddingHorizontal: 16, gap: 16 },
+  imageDescription: { color: Colors.WHITE, textAlign: 'center', fontWeight: '600' },
+  imageOwner: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    justifyContent: 'center',
+    gap: 4,
+    backgroundColor: 'rgba(255, 255, 255, 0.8)',
+    paddingVertical: 8,
+    paddingHorizontal: 12,
+    borderRadius: 8,
+    alignSelf: 'center'
+  },
+  imageOwnerText: { color: Colors.DARK_BLUE, fontFamily: 'montserrat-700' },
+  wrapper: {
+    marginLeft: '5%',
+    marginRight: '5%',
+    gap: 16
+  },
+  divider: {
+    height: 1,
+    width: '100%',
+    backgroundColor: Colors.DARK_LIGHT
+  },
+  nameContainer: {
+    flexDirection: 'row',
+    justifyContent: 'space-between',
+    alignItems: 'center'
+  },
+  title: {
+    fontSize: 18,
+    fontFamily: 'montserrat-700',
+    color: Colors.DARK_BLUE,
+    flex: 1
+  },
+  subtitle: {
+    fontSize: 12,
+    fontWeight: '600',
+    color: Colors.DARK_BLUE
+  },
+  stats: {
+    flexDirection: 'row',
+    justifyContent: 'space-between'
+  },
+  icon: {
+    width: 28,
+    height: 28,
+    borderRadius: 14,
+    backgroundColor: Colors.DARK_BLUE,
+    alignItems: 'center',
+    justifyContent: 'center'
+  },
+  statItem: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    gap: 4,
+    flex: 1
+  },
+  statText: {
+    fontSize: 14,
+    color: 'gray'
+  },
+  statNumber: {
+    fontSize: 16,
+    fontWeight: 'bold'
+  },
+  travelSeriesTitle: {
+    fontSize: 14,
+    fontFamily: 'montserrat-700',
+    textTransform: 'uppercase',
+    color: Colors.DARK_BLUE
+  },
+  userImageContainer: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    gap: 4
+  },
+  userImage: {
+    width: 28,
+    height: 28,
+    borderRadius: 14,
+    marginLeft: -6,
+    borderWidth: 1,
+    borderColor: Colors.DARK_LIGHT,
+    resizeMode: 'cover'
+  },
+  userCountContainer: {
+    width: 28,
+    height: 28,
+    borderRadius: 14,
+    backgroundColor: Colors.DARK_LIGHT,
+    alignItems: 'center',
+    justifyContent: 'center',
+    marginLeft: -6
+  },
+  userCount: {
+    fontSize: 12,
+    color: Colors.DARK_BLUE,
+    lineHeight: 24
+  },
+  modalView: {
+    paddingHorizontal: 8,
+    paddingVertical: 24,
+    alignItems: 'center'
+  },
+  infoTitle: {
+    color: Colors.DARK_BLUE,
+    fontSize: 16,
+    fontWeight: '700',
+    textAlign: 'center',
+    marginBottom: 16
+  },
+  infoText: {
+    color: Colors.DARK_BLUE,
+    fontSize: 14,
+    fontWeight: '400',
+    textAlign: 'left',
+    marginBottom: 24
+  },
+  btnContainer: {
+    borderColor: Colors.DARK_BLUE,
+    backgroundColor: Colors.WHITE,
+    width: '60%'
+  },
+  infoContent: { flexDirection: 'row', alignItems: 'center', gap: 4 },
+  visitedButtonText: {
+    color: Colors.DARK_BLUE,
+    fontWeight: 'bold',
+    fontSize: 13
+  },
+  durationContainer: {
+    flexDirection: 'row',
+    justifyContent: 'center',
+    gap: 12
+  },
+  durationItem: {
+    alignItems: 'center',
+    justifyContent: 'center',
+    gap: 4
+  },
+  durationIconActive: {
+    backgroundColor: Colors.WHITE
+  },
+  durationIconInactive: {
+    backgroundColor: Colors.LIGHT_GRAY
+  },
+  visitDuration: {
+    color: Colors.DARK_BLUE,
+    fontWeight: '600',
+    fontSize: getFontSize(11)
+  },
+  goToMapBtn: {
+    position: 'absolute',
+    width: 50,
+    height: 50,
+    top: 50,
+    right: 10,
+    justifyContent: 'center',
+    alignItems: 'center',
+    zIndex: 2
+  },
+  addPhotoButton: {
+    position: 'absolute',
+    width: 50,
+    height: 50,
+    top: 160,
+    right: 10,
+    justifyContent: 'center',
+    alignItems: 'center',
+    zIndex: 2
+  }
+});

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

@@ -15,6 +15,7 @@ import EarthIcon from '../../../../assets/icons/travels-section/earth.svg';
 import TripIcon from '../../../../assets/icons/travels-section/trip.svg';
 import ImagesIcon from '../../../../assets/icons/travels-section/images.svg';
 import FixersIcon from '../../../../assets/icons/travels-section/fixers.svg';
+import CalendarIcon from 'assets/icons/events/calendar-solid.svg';
 import InfoIcon from 'assets/icons/info-solid.svg';
 import { getFontSize } from 'src/utils';
 import { useSafeAreaInsets } from 'react-native-safe-area-context';
@@ -33,7 +34,8 @@ const TravelsScreen = () => {
     { label: 'Earth', icon: EarthIcon, page: NAVIGATION_PAGES.EARTH },
     { label: 'Trips', icon: TripIcon, page: NAVIGATION_PAGES.TRIPS },
     { label: 'Photos', icon: ImagesIcon, page: NAVIGATION_PAGES.PHOTOS },
-    { label: 'Fixers', icon: FixersIcon, page: NAVIGATION_PAGES.FIXERS }
+    { label: 'Fixers', icon: FixersIcon, page: NAVIGATION_PAGES.FIXERS },
+    { label: 'Events', icon: CalendarIcon, page: NAVIGATION_PAGES.EVENTS }
   ];
 
   const handlePress = (page: string) => {

+ 1 - 0
src/types/navigation.ts

@@ -71,4 +71,5 @@ export enum NAVIGATION_PAGES {
   CHATS_LIST = 'inAppChatsList',
   CHAT = 'inAppChat',
   LOCATION_SHARING = 'inAppLocationSharing',
+  EVENTS = 'inAppEvents',
 }