Explorar o código

series linking

Viktoriia hai 1 mes
pai
achega
439b83d226

+ 3 - 2
App.tsx

@@ -77,11 +77,12 @@ Sentry.init({
 });
 
 const linking = {
-  prefixes: [API_HOST, 'nomadmania://'],
+  prefixes: [API_HOST, 'nomadmania://', 'nm://'],
   config: {
     screens: {
       publicProfileView: '/profile/:userId',
       inAppEvent: '/event/:url',
+      inAppSeriesShare: '/series-item/:id',
       inAppMapTab: '/map/:lon/:lat',
       inAppChatsList: '/messages',
       inAppMasterRanking: '/master-ranking',
@@ -131,7 +132,7 @@ const App = () => {
 
 const InnerApp = () => {
   const errorContext = useError();
-  const navigation = React.useRef(null);
+  const navigation = React.useRef<any>(null);
   const [isUpdateAvailable, setIsUpdateAvailable] = useState(false);
 
   useEffect(() => {

+ 6 - 0
Route.tsx

@@ -51,6 +51,8 @@ import DareScreen from 'src/screens/InAppScreens/TravelsScreen/DareScreen';
 import FixersScreen from 'src/screens/InAppScreens/TravelsScreen/FixersScreen';
 import AddNewFixerScreen from 'src/screens/InAppScreens/TravelsScreen/AddNewFixerScreen';
 import FixersCommentsScreen from 'src/screens/InAppScreens/TravelsScreen/FixersCommentsScreen';
+import { SeriesShareScreen } from 'src/screens/InAppScreens/TravelsScreen/SeriesShareScreen';
+
 
 import { API, NAVIGATION_PAGES } from './src/types';
 import { storage, StoreType } from './src/storage';
@@ -750,6 +752,10 @@ const Route = () => {
           <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.SERIES_SHARE}
+            component={SeriesShareScreen}
+          />
           <ScreenStack.Screen name={NAVIGATION_PAGES.IN_APP}>
             {() => (
               <MapDrawer.Navigator drawerContent={(props) => <MenuDrawer {...props} />}>

+ 13 - 6
app.config.ts

@@ -18,11 +18,18 @@ dotenv.config({
   path: path.resolve(process.cwd(), '.env')
 });
 
-const intentData = DEEP_LINK_PATHS.map((pathPrefix: string) => ({
-  scheme: 'https',
-  host: 'nomadmania.com',
-  pathPrefix,
-}));
+const intentData = [
+  ...DEEP_LINK_PATHS.map((pathPrefix: string) => ({
+    scheme: 'https',
+    host: 'nomadmania.com',
+    pathPrefix,
+  })),
+  ...DEEP_LINK_PATHS.map((pathPrefix: string) => ({
+    scheme: 'https',
+    host: 'nomadmania.eu',
+    pathPrefix,
+  }))
+];
 
 export default ({ config }: ConfigContext): ExpoConfig => ({
   ...config,
@@ -76,7 +83,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
     config: {
       googleMapsApiKey: env.IOS_GOOGLE_MAP_APIKEY
     },
-    associatedDomains: ['applinks:nomadmania.com'],
+    associatedDomains: ['applinks:nomadmania.com', 'applinks:nomadmania.eu'],
     infoPlist: {
       UIBackgroundModes: ['location', 'fetch', 'remote-notification'],
       NSLocationAlwaysUsageDescription:

+ 1 - 0
config/deepLinks.js

@@ -23,5 +23,6 @@ module.exports = {
     '/region_mqp/',
     '/photos/',
     '/trips/',
+    '/series-item/',
   ],
 };

+ 1 - 0
src/modules/api/series/queries/index.ts

@@ -11,3 +11,4 @@ export * from './use-post-submit-suggestion';
 export * from './use-post-get-list';
 export * from './use-get-icons';
 export * from './use-post-get-users-who-ticked-series';
+export * from './use-post-get-single-item';

+ 22 - 0
src/modules/api/series/queries/use-post-get-single-item.tsx

@@ -0,0 +1,22 @@
+import { useQuery } from '@tanstack/react-query';
+import { useMutation } from '@tanstack/react-query';
+
+import { seriesQueryKeys } from '../series-query-keys';
+import { seriesApi, type PostGetSingleSeriesItem } from '../series-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const useGetSingleSeriesItemMutation = () => {
+  return useMutation<
+    PostGetSingleSeriesItem,
+    BaseAxiosError,
+    { id: number; },
+    PostGetSingleSeriesItem
+  >({
+    mutationKey: seriesQueryKeys.getSingleSeriesItem(),
+    mutationFn: async (variables) => {
+      const response = await seriesApi.getSingleSeriesItem(variables.id);
+      return response.data;
+    }
+  });
+};

+ 18 - 1
src/modules/api/series/series-api.tsx

@@ -174,6 +174,21 @@ export interface PostGetSeriesIcons extends ResponseType {
   }[];
 }
 
+export interface PostGetSingleSeriesItem extends ResponseType {
+  data: {
+    item_id: number;
+    item_name: string;
+    location_geojson: string | null;
+    region_id: number;
+    region_name: string;
+    series_icon: string | null;
+    series_id: number;
+    series_name: string;
+    visited: 0 | 1;
+    series_group_id: number;
+  }[];
+}
+
 export const seriesApi = {
   getSeries: (token: string | null, regions: string) =>
     request.postForm<PostGetSeries>(API.SERIES, { token, regions }),
@@ -226,5 +241,7 @@ export const seriesApi = {
       sort,
       age,
       country
-    })
+    }),
+  getSingleSeriesItem: (id: number) =>
+    request.postForm<PostGetSingleSeriesItem>(API.GET_SINGLE_SERIES_ITEM, { id })
 };

+ 2 - 1
src/modules/api/series/series-query-keys.tsx

@@ -14,5 +14,6 @@ export const seriesQueryKeys = {
   submitSuggestion: () => ['submitSuggestion'] as const,
   getList: () => ['getList'] as const,
   getIcons: () => ['getIcons'] as const,
-  getUsersWhoTickedSeries: () => ['getUsersWhoTickedSeries'] as const
+  getUsersWhoTickedSeries: () => ['getUsersWhoTickedSeries'] as const,
+  getSingleSeriesItem: () => ['getSingleSeriesItem'] as const
 };

+ 90 - 53
src/screens/InAppScreens/MapScreen/MarkerItem/index.tsx

@@ -1,10 +1,11 @@
 import { useEffect, useRef } from 'react';
-import { View, Image, Text, TouchableOpacity, Platform } from 'react-native';
+import { View, Image, Text, TouchableOpacity, Platform, Share } from 'react-native';
 
 import { styles } from './styles';
 import { Colors } from 'src/theme';
 
 import CheckSvg from 'assets/icons/mark.svg';
+import ShareIcon from 'assets/icons/share.svg';
 import * as MapLibreRN from '@maplibre/maplibre-react-native';
 import { API_HOST } from 'src/constants';
 import { NAVIGATION_PAGES } from 'src/types';
@@ -51,11 +52,13 @@ const MarkerItem = ({
           <View style={styles.customView}>
             <View style={styles.calloutContainer}>
               <View style={styles.calloutImageContainer}>
-                <Image
-                  source={{ uri: marker.icon?.uri || '' }}
-                  style={styles.calloutImage}
-                  resizeMode="contain"
-                />
+                {!!marker.icon?.uri && (
+                  <Image
+                    source={{ uri: marker.icon.uri }}
+                    style={styles.calloutImage}
+                    resizeMode="contain"
+                  />
+                )}
               </View>
               <View style={styles.calloutTextContainer}>
                 <Text style={styles.seriesName}>{marker.series_name}</Text>
@@ -64,33 +67,49 @@ const MarkerItem = ({
                 </Text>
               </View>
 
-              <TouchableOpacity
-                style={[
-                  styles.calloutButton,
-                  (marker.visited === 1 &&
-                    token && {
+              <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
+                <TouchableOpacity
+                  style={[
+                    styles.calloutButton,
+                    { flex: 1 },
+                    (marker.visited === 1 &&
+                      token && {
                       backgroundColor: Colors.WHITE,
                       borderWidth: 1,
                       borderColor: Colors.BORDER_LIGHT
                     }) ||
                     {}
-                ]}
-                onPress={() => toggleSeries(marker)}
-              >
-                <View style={styles.completedContainer}>
-                  <View style={{ display: marker?.visited === 1 && token ? 'flex' : 'none' }}>
-                    <CheckSvg width={14} height={14} fill={Colors.DARK_BLUE} />
+                  ]}
+                  onPress={() => toggleSeries(marker)}
+                >
+                  <View style={styles.completedContainer}>
+                    <View style={{ display: marker?.visited === 1 && token ? 'flex' : 'none' }}>
+                      <CheckSvg width={14} height={14} fill={Colors.DARK_BLUE} />
+                    </View>
+                    <Text
+                      style={[
+                        styles.calloutButtonText,
+                        marker?.visited === 1 && token ? { color: Colors.DARK_BLUE } : {}
+                      ]}
+                    >
+                      {marker?.visited === 1 && token ? 'Completed' : 'To Complete'}
+                    </Text>
                   </View>
-                  <Text
-                    style={[
-                      styles.calloutButtonText,
-                      marker?.visited === 1 && token ? { color: Colors.DARK_BLUE } : {}
-                    ]}
-                  >
-                    {marker?.visited === 1 && token ? 'Completed' : 'To Complete'}
-                  </Text>
-                </View>
-              </TouchableOpacity>
+                </TouchableOpacity>
+
+                <TouchableOpacity
+                  onPress={() => {
+                    Share.share(
+                      Platform.OS === 'ios'
+                        ? { url: `${API_HOST}/series-item/${marker.id}` }
+                        : { message: `${API_HOST}/series-item/${marker.id}` }
+                    );
+                  }}
+                  style={{ padding: 6, backgroundColor: Colors.FILL_LIGHT, borderRadius: 8, height: 30, marginBottom: 12 }}
+                >
+                  <ShareIcon width={18} height={18} fill={Colors.DARK_BLUE} />
+                </TouchableOpacity>
+              </View>
 
               {parsedAvatars && (
                 <TouchableOpacity
@@ -137,11 +156,13 @@ const MarkerItem = ({
           <View style={styles.customView}>
             <View style={styles.calloutContainer}>
               <View style={styles.calloutImageContainer}>
-                <Image
-                  source={{ uri: marker.icon?.uri || '' }}
-                  style={styles.calloutImage}
-                  resizeMode="contain"
-                />
+                {!!marker.icon?.uri && (
+                  <Image
+                    source={{ uri: marker.icon.uri }}
+                    style={styles.calloutImage}
+                    resizeMode="contain"
+                  />
+                )}
               </View>
               <View style={[styles.calloutTextContainer, { flex: 0 }]}>
                 <Text style={styles.seriesName}>{marker.series_name}</Text>
@@ -150,33 +171,49 @@ const MarkerItem = ({
                 </Text>
               </View>
 
-              <TouchableOpacity
-                style={[
-                  styles.calloutButton,
-                  (marker.visited === 1 &&
-                    token && {
+              <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
+                <TouchableOpacity
+                  style={[
+                    styles.calloutButton,
+                    { flex: 1 },
+                    (marker.visited === 1 &&
+                      token && {
                       backgroundColor: Colors.WHITE,
                       borderWidth: 1,
                       borderColor: Colors.BORDER_LIGHT
                     }) ||
                     {}
-                ]}
-                onPressIn={() => toggleSeries(marker)}
-              >
-                <View style={styles.completedContainer}>
-                  <View style={{ display: marker?.visited === 1 && token ? 'flex' : 'none' }}>
-                    <CheckSvg width={14} height={14} fill={Colors.DARK_BLUE} />
+                  ]}
+                  onPressIn={() => toggleSeries(marker)}
+                >
+                  <View style={styles.completedContainer}>
+                    <View style={{ display: marker?.visited === 1 && token ? 'flex' : 'none' }}>
+                      <CheckSvg width={14} height={14} fill={Colors.DARK_BLUE} />
+                    </View>
+                    <Text
+                      style={[
+                        styles.calloutButtonText,
+                        marker?.visited === 1 && token ? { color: Colors.DARK_BLUE } : {}
+                      ]}
+                    >
+                      {marker?.visited === 1 && token ? 'Completed' : 'To Complete'}
+                    </Text>
                   </View>
-                  <Text
-                    style={[
-                      styles.calloutButtonText,
-                      marker?.visited === 1 && token ? { color: Colors.DARK_BLUE } : {}
-                    ]}
-                  >
-                    {marker?.visited === 1 && token ? 'Completed' : 'To Complete'}
-                  </Text>
-                </View>
-              </TouchableOpacity>
+                </TouchableOpacity>
+
+                <TouchableOpacity
+                  onPressIn={() => {
+                    Share.share(
+                      Platform.OS === 'ios'
+                        ? { url: `${API_HOST}/series-item/${marker.id}` }
+                        : { message: `${API_HOST}/series-item/${marker.id}` }
+                    );
+                  }}
+                  style={{ padding: 6, backgroundColor: Colors.FILL_LIGHT, borderRadius: 8, height: 30, marginBottom: 12 }}
+                >
+                  <ShareIcon width={18} height={18} fill={Colors.DARK_BLUE} />
+                </TouchableOpacity>
+              </View>
 
               {parsedAvatars && (
                 <TouchableOpacity

+ 31 - 16
src/screens/InAppScreens/MapScreen/MultipleSeriesModal/index.tsx

@@ -1,6 +1,7 @@
 import React, { useRef, useState } from 'react';
-import { View, Text, Image, StyleSheet, TouchableOpacity, Platform } from 'react-native';
+import { View, Text, Image, StyleSheet, TouchableOpacity, Platform, Share } from 'react-native';
 import ActionSheet, { SheetManager } from 'react-native-actions-sheet';
+import ShareIcon from '../../../../../assets/icons/share.svg';
 import { Colors } from 'src/theme';
 import { getFontSize } from 'src/utils';
 import { FlashList } from '@shopify/flash-list';
@@ -44,7 +45,6 @@ const MultipleSeriesModal = () => {
 
       return;
     }
-    seriesData.setSelectedMarker(item);
 
     seriesData.toggleSeries(item);
     setMarkers((prevMarkers) =>
@@ -157,20 +157,35 @@ const MultipleSeriesModal = () => {
                         </TouchableOpacity>
                       )}
 
-                      <TouchableOpacity
-                        onPress={() => handleToggleSeries(item)}
-                        style={[styles.markButton, item.visited === 1 && styles.visitedButton]}
-                      >
-                        {item.visited === 1 ? (
-                          <View style={styles.completedContainer}>
-                            <Text style={[styles.calloutButtonText, { color: Colors.DARK_BLUE }]}>
-                              Completed
-                            </Text>
-                          </View>
-                        ) : (
-                          <Text style={styles.calloutButtonText}>To Complete</Text>
-                        )}
-                      </TouchableOpacity>
+                      <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
+                        <TouchableOpacity
+                          onPress={() => handleToggleSeries(item)}
+                          style={[styles.markButton, item.visited === 1 && styles.visitedButton]}
+                        >
+                          {item.visited === 1 ? (
+                            <View style={styles.completedContainer}>
+                              <Text style={[styles.calloutButtonText, { color: Colors.DARK_BLUE }]}>
+                                Completed
+                              </Text>
+                            </View>
+                          ) : (
+                            <Text style={styles.calloutButtonText}>To Complete</Text>
+                          )}
+                        </TouchableOpacity>
+                        
+                        <TouchableOpacity
+                          onPress={() => {
+                            Share.share(
+                              Platform.OS === 'ios'
+                                ? { url: `${API_HOST}/series-item/${item.id}` }
+                                : { message: `${API_HOST}/series-item/${item.id}` }
+                            );
+                          }}
+                          style={{ padding: 6, backgroundColor: Colors.WHITE, borderRadius: 8, borderWidth: 1, borderColor: Colors.BORDER_LIGHT }}
+                        >
+                          <ShareIcon width={16} height={16} fill={Colors.DARK_BLUE} />
+                        </TouchableOpacity>
+                      </View>
                     </View>
                   </View>
                 </TouchableOpacity>

+ 36 - 18
src/screens/InAppScreens/MapScreen/UniversalSearch/index.tsx

@@ -1,5 +1,5 @@
 import React, { useCallback, useEffect, useRef, useState } from 'react';
-import { View, Text, TouchableOpacity, Image, Dimensions } from 'react-native';
+import { View, Text, TouchableOpacity, Image, Dimensions, Share, Platform } from 'react-native';
 import { FlashList } from '@shopify/flash-list';
 import { useNavigation } from '@react-navigation/native';
 
@@ -11,6 +11,7 @@ import MessagesDot from 'src/components/MessagesDot';
 import ActionSheet, { ActionSheetRef } from 'react-native-actions-sheet';
 import TabViewWrapper from 'src/components/TabViewWrapper';
 import LocationIcon from 'assets/icons/travels-screens/map-location.svg';
+import ShareIcon from 'assets/icons/share.svg';
 import { usePostSetToggleItem } from '@api/series';
 import { Colors } from 'src/theme';
 
@@ -138,9 +139,11 @@ const SearchModal = ({
             </View>
 
             <View style={{ flex: 1, gap: 8 }}>
-              <View style={{ justifyContent: 'space-between', flex: 1 }}>
-                <Text style={styles.seriesName}>{item.series_name}</Text>
-                <Text style={styles.seriesDescription}>{item.item_name}</Text>
+              <View style={{ justifyContent: 'space-between', flex: 1, flexDirection: 'row' }}>
+                <View style={{ flex: 1 }}>
+                  <Text style={styles.seriesName}>{item.series_name}</Text>
+                  <Text style={styles.seriesDescription}>{item.item_name}</Text>
+                </View>
               </View>
 
               <View
@@ -166,20 +169,35 @@ const SearchModal = ({
                   </TouchableOpacity>
                 ) : <View/>}
 
-                <TouchableOpacity
-                  onPress={() => toggleSeries(item)}
-                  style={[styles.markButton, isCompleted && styles.visitedButton]}
-                >
-                  {isCompleted ? (
-                    <View style={styles.completedContainer}>
-                      <Text style={[styles.calloutButtonText, { color: Colors.DARK_BLUE }]}>
-                        Completed
-                      </Text>
-                    </View>
-                  ) : (
-                    <Text style={styles.calloutButtonText}>To Complete</Text>
-                  )}
-                </TouchableOpacity>
+                <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
+                  <TouchableOpacity
+                    onPress={() => toggleSeries(item)}
+                    style={[styles.markButton, isCompleted && styles.visitedButton]}
+                  >
+                    {isCompleted ? (
+                      <View style={styles.completedContainer}>
+                        <Text style={[styles.calloutButtonText, { color: Colors.DARK_BLUE }]}>
+                          Completed
+                        </Text>
+                      </View>
+                    ) : (
+                      <Text style={styles.calloutButtonText}>To Complete</Text>
+                    )}
+                  </TouchableOpacity>
+
+                  <TouchableOpacity
+                    onPress={() => {
+                      Share.share(
+                        Platform.OS === 'ios'
+                          ? { url: `${API_HOST}/series-item/${item.item_id}` }
+                          : { message: `${API_HOST}/series-item/${item.item_id}` }
+                      );
+                    }}
+                    style={{ padding: 6, backgroundColor: Colors.WHITE, borderRadius: 8, borderWidth: 1, borderColor: Colors.BORDER_LIGHT }}
+                  >
+                    <ShareIcon width={16} height={16} fill={Colors.DARK_BLUE} />
+                  </TouchableOpacity>
+                </View>
               </View>
             </View>
           </View>

+ 232 - 211
src/screens/InAppScreens/MapScreen/index.tsx

@@ -436,6 +436,8 @@ const SERIES_TEXT_SIZE = [
   15
 ] as unknown as number;
 
+const processedSeriesShares = new Set<number>();
+
 const MapScreen: any = ({ navigation, route }: { navigation: any; route: any }) => {
   const tabBarHeight = useBottomTabBarHeight();
   const userId = storage.get('uid', StoreType.STRING) as string;
@@ -585,6 +587,7 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
   const [renderCamera, setRenderCamera] = useState(Platform.OS === 'ios');
   const isAnimatingRef = useRef(false);
   const animationTimeoutRef = useRef<NodeJS.Timeout | null>(null);
+  const handledSeriesShareRef = useRef<number | null>(null);
 
   const [markerCoords, setMarkerCoords] = useState<any>(null);
   const [refreshInterval, setRefreshInterval] = useState(0);
@@ -997,7 +1000,16 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
 
       return () => clearTimeout(timeoutId);
     }
-    if (route.params?.id && route.params?.type && db1 && db2 && db3) {
+    if (route.params?.seriesItemShare) {
+      const itemToFind = route.params.seriesItemShare;
+      const shareId = itemToFind._timestamp || itemToFind.item_id;
+      if (!processedSeriesShares.has(shareId)) {
+        processedSeriesShares.add(shareId);
+        handledSeriesShareRef.current = shareId;
+        navigation.setParams({ seriesItemShare: undefined });
+        handleFindSeries(itemToFind);
+      }
+    } else if (route.params?.id && route.params?.type && db1 && db2 && db3) {
       handleFindRegion(route.params?.id, route.params?.type);
     }
   }, [route, db1, db2, db3]);
@@ -1007,6 +1019,11 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
       if (token) {
         refetch();
       }
+      return () => {
+        if (handledSeriesShareRef.current !== null) {
+          setSelectedMarker(null);
+        }
+      };
     }, [])
   );
 
@@ -1818,7 +1835,7 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
     try {
       if (seriesItem.location_geojson) {
         const geojsonObj = JSON.parse(seriesItem.location_geojson);
-        
+
         if (geojsonObj.type === 'Point') {
           const parsedCoords = geojsonObj.coordinates;
           if (parsedCoords && parsedCoords.length === 2) {
@@ -1861,17 +1878,21 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
     }
 
     if (coordinates) {
-      cameraController.setCamera({
-        centerCoordinate: coordinates,
-        zoomLevel: 10,
-        animationDuration: 1000,
-        animationMode: 'flyTo'
-      });
+      setTimeout(() => {
+        cameraController.setCamera({
+          centerCoordinate: coordinates,
+          zoomLevel: 10,
+          animationDuration: 1000,
+          animationMode: 'flyTo'
+        });
+      }, 800);
+
+      handledSeriesShareRef.current = seriesItem.item_id;
 
       setSelectedMarker({
         coordinates,
         name: seriesItem.item_name,
-        icon: { uri: API_HOST + seriesItem.series_icon },
+        icon: seriesItem.series_icon ? { uri: API_HOST + seriesItem.series_icon } : null,
         description: seriesItem.series_name,
         series_name: seriesItem.series_name,
         visited: seriesItem.visited,
@@ -1886,7 +1907,7 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
         try {
           if (!mapRef.current) return;
           const pointInView = await mapRef.current.getPointInView(coordinates);
-          
+
           if (pointInView) {
             const { features } = await mapRef.current.queryRenderedFeaturesAtPoint(
               pointInView,
@@ -1970,7 +1991,7 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
           return {
             coordinates,
             name,
-            icon,
+            icon: icon || (isSameMarker ? prev.icon : null),
             description,
             series_name,
             visited: currentVisited,
@@ -2444,210 +2465,210 @@ const MapScreen: any = ({ navigation, route }: { navigation: any; route: any })
         >
           {seriesFilter.status !== 1
             ? (() => {
-                try {
-                  return [
-                    <MapLibreRN.SymbolLayer
-                      key="symbol_unvisited_normal"
-                      id={`${series_layer.id}_normal`}
-                      sourceID={series_layer.source}
-                      sourceLayerID={series_layer['source-layer']}
-                      aboveLayerID={'waterway-name'}
-                      filter={['all', seriesNotVisitedFilter, ['!=', 'must', 1]] as any}
-                      minZoomLevel={series_layer.minzoom}
-                      maxZoomLevel={series_layer.maxzoom}
-                      style={{
-                        symbolSpacing: 1,
-                        iconImage: '{series_id}',
-                        iconAllowOverlap: true,
-                        iconIgnorePlacement: true,
-                        iconSize: SERIES_ICON_SIZE,
-                        visibility: 'visible',
-                        iconColor: '#666',
-                        iconOpacity: 1,
-                        iconHaloColor: '#ffffff',
-                        iconHaloWidth: 1,
-                        iconHaloBlur: 0.5,
-                        textAnchor: 'top',
-                        textField: seriesTextField,
-                        textFont: ['Noto Sans Regular'],
-                        textMaxWidth: 9,
-                        textOffset: [0, 1.3],
-                        textPadding: 2,
-                        textSize: SERIES_TEXT_SIZE,
-                        textOptional: true,
-                        textIgnorePlacement: false,
-                        textAllowOverlap: false,
-                        textColor: '#666',
-                        textHaloColor: '#ffffff',
-                        textHaloWidth: 1,
-                        textHaloBlur: 0.5
-                      }}
-                    />,
-                    <MapLibreRN.CircleLayer
-                      key="circle_unvisited_must"
-                      id={`${series_layer.id}_must_wrapper`}
-                      sourceID={series_layer.source}
-                      sourceLayerID={series_layer['source-layer']}
-                      aboveLayerID={`${series_layer.id}_normal`}
-                      filter={['all', seriesNotVisitedFilter, ['==', 'must', 1]] as any}
-                      minZoomLevel={series_layer.minzoom}
-                      maxZoomLevel={series_layer.maxzoom}
-                      style={{
-                        circleRadius: SERIES_CIRCLE_RADIUS,
-                        circleColor: Colors.ORANGE,
-                        circleOpacity: 1,
-                        circleStrokeWidth: SERIES_CIRCLE_STROKE,
-                        circleStrokeColor: Colors.ORANGE,
-                        visibility: 'visible'
-                      }}
-                    />,
-                    <MapLibreRN.SymbolLayer
-                      key="symbol_unvisited_must"
-                      id={series_layer.id}
-                      sourceID={series_layer.source}
-                      sourceLayerID={series_layer['source-layer']}
-                      aboveLayerID={`${series_layer.id}_must_wrapper`}
-                      filter={['all', seriesNotVisitedFilter, ['==', 'must', 1]] as any}
-                      minZoomLevel={series_layer.minzoom}
-                      maxZoomLevel={series_layer.maxzoom}
-                      style={{
-                        symbolSpacing: 1,
-                        iconImage: '{series_id}',
-                        iconAllowOverlap: true,
-                        iconIgnorePlacement: true,
-                        iconSize: SERIES_ICON_SIZE,
-                        visibility: 'visible',
-                        iconColor: '#666',
-                        iconOpacity: 1,
-                        iconHaloColor: '#ffffff',
-                        iconHaloWidth: 1,
-                        iconHaloBlur: 0.5,
-                        textAnchor: 'top',
-                        textField: seriesTextField,
-                        textFont: ['Noto Sans Regular'],
-                        textMaxWidth: 9,
-                        textOffset: [0, 1.3],
-                        textPadding: 2,
-                        textSize: SERIES_TEXT_SIZE,
-                        textOptional: true,
-                        textIgnorePlacement: false,
-                        textAllowOverlap: false,
-                        textColor: '#666',
-                        textHaloColor: '#ffffff',
-                        textHaloWidth: 1,
-                        textHaloBlur: 0.5
-                      }}
-                    />
-                  ];
-                } catch (error) {
-                  console.warn('SymbolLayer render error:', error);
-                  return null;
-                }
-              })()
+              try {
+                return [
+                  <MapLibreRN.SymbolLayer
+                    key="symbol_unvisited_normal"
+                    id={`${series_layer.id}_normal`}
+                    sourceID={series_layer.source}
+                    sourceLayerID={series_layer['source-layer']}
+                    aboveLayerID={'waterway-name'}
+                    filter={['all', seriesNotVisitedFilter, ['!=', 'must', 1]] as any}
+                    minZoomLevel={series_layer.minzoom}
+                    maxZoomLevel={series_layer.maxzoom}
+                    style={{
+                      symbolSpacing: 1,
+                      iconImage: '{series_id}',
+                      iconAllowOverlap: true,
+                      iconIgnorePlacement: true,
+                      iconSize: SERIES_ICON_SIZE,
+                      visibility: 'visible',
+                      iconColor: '#666',
+                      iconOpacity: 1,
+                      iconHaloColor: '#ffffff',
+                      iconHaloWidth: 1,
+                      iconHaloBlur: 0.5,
+                      textAnchor: 'top',
+                      textField: seriesTextField,
+                      textFont: ['Noto Sans Regular'],
+                      textMaxWidth: 9,
+                      textOffset: [0, 1.3],
+                      textPadding: 2,
+                      textSize: SERIES_TEXT_SIZE,
+                      textOptional: true,
+                      textIgnorePlacement: false,
+                      textAllowOverlap: false,
+                      textColor: '#666',
+                      textHaloColor: '#ffffff',
+                      textHaloWidth: 1,
+                      textHaloBlur: 0.5
+                    }}
+                  />,
+                  <MapLibreRN.CircleLayer
+                    key="circle_unvisited_must"
+                    id={`${series_layer.id}_must_wrapper`}
+                    sourceID={series_layer.source}
+                    sourceLayerID={series_layer['source-layer']}
+                    aboveLayerID={`${series_layer.id}_normal`}
+                    filter={['all', seriesNotVisitedFilter, ['==', 'must', 1]] as any}
+                    minZoomLevel={series_layer.minzoom}
+                    maxZoomLevel={series_layer.maxzoom}
+                    style={{
+                      circleRadius: SERIES_CIRCLE_RADIUS,
+                      circleColor: Colors.ORANGE,
+                      circleOpacity: 1,
+                      circleStrokeWidth: SERIES_CIRCLE_STROKE,
+                      circleStrokeColor: Colors.ORANGE,
+                      visibility: 'visible'
+                    }}
+                  />,
+                  <MapLibreRN.SymbolLayer
+                    key="symbol_unvisited_must"
+                    id={series_layer.id}
+                    sourceID={series_layer.source}
+                    sourceLayerID={series_layer['source-layer']}
+                    aboveLayerID={`${series_layer.id}_must_wrapper`}
+                    filter={['all', seriesNotVisitedFilter, ['==', 'must', 1]] as any}
+                    minZoomLevel={series_layer.minzoom}
+                    maxZoomLevel={series_layer.maxzoom}
+                    style={{
+                      symbolSpacing: 1,
+                      iconImage: '{series_id}',
+                      iconAllowOverlap: true,
+                      iconIgnorePlacement: true,
+                      iconSize: SERIES_ICON_SIZE,
+                      visibility: 'visible',
+                      iconColor: '#666',
+                      iconOpacity: 1,
+                      iconHaloColor: '#ffffff',
+                      iconHaloWidth: 1,
+                      iconHaloBlur: 0.5,
+                      textAnchor: 'top',
+                      textField: seriesTextField,
+                      textFont: ['Noto Sans Regular'],
+                      textMaxWidth: 9,
+                      textOffset: [0, 1.3],
+                      textPadding: 2,
+                      textSize: SERIES_TEXT_SIZE,
+                      textOptional: true,
+                      textIgnorePlacement: false,
+                      textAllowOverlap: false,
+                      textColor: '#666',
+                      textHaloColor: '#ffffff',
+                      textHaloWidth: 1,
+                      textHaloBlur: 0.5
+                    }}
+                  />
+                ];
+              } catch (error) {
+                console.warn('SymbolLayer render error:', error);
+                return null;
+              }
+            })()
             : null}
 
           {seriesFilter.status !== 0
             ? (() => {
-                try {
-                  return [
-                    <MapLibreRN.SymbolLayer
-                      key="symbol_visited_normal"
-                      id={`${series_visited.id}_normal`}
-                      sourceID={series_visited.source}
-                      sourceLayerID={series_visited['source-layer']}
-                      aboveLayerID={'waterway-name'}
-                      filter={['all', seriesVisitedFilter, ['!=', 'must', 1]] as any}
-                      minZoomLevel={series_visited.minzoom}
-                      maxZoomLevel={series_visited.maxzoom}
-                      style={{
-                        symbolSpacing: 1,
-                        iconImage: '{series_id}v',
-                        iconAllowOverlap: true,
-                        iconIgnorePlacement: true,
-                        iconSize: SERIES_ICON_SIZE,
-                        visibility: 'visible',
-                        iconColor: '#666',
-                        iconOpacity: 1,
-                        iconHaloColor: '#ffffff',
-                        iconHaloWidth: 1,
-                        iconHaloBlur: 0.5,
-                        textAnchor: 'top',
-                        textField: seriesTextField,
-                        textFont: ['Noto Sans Regular'],
-                        textMaxWidth: 9,
-                        textOffset: [0, 1.3],
-                        textPadding: 2,
-                        textSize: SERIES_TEXT_SIZE,
-                        textOptional: true,
-                        textIgnorePlacement: false,
-                        textAllowOverlap: false,
-                        textColor: '#666',
-                        textHaloColor: '#ffffff',
-                        textHaloWidth: 1,
-                        textHaloBlur: 0.5
-                      }}
-                    />,
-                    <MapLibreRN.CircleLayer
-                      key="circle_visited_must"
-                      id={`${series_visited.id}_must_wrapper`}
-                      sourceID={series_visited.source}
-                      sourceLayerID={series_visited['source-layer']}
-                      aboveLayerID={`${series_visited.id}_normal`}
-                      filter={['all', seriesVisitedFilter, ['==', 'must', 1]] as any}
-                      minZoomLevel={series_visited.minzoom}
-                      maxZoomLevel={series_visited.maxzoom}
-                      style={{
-                        circleRadius: SERIES_CIRCLE_RADIUS,
-                        circleColor: Colors.ORANGE,
-                        circleOpacity: 1,
-                        circleStrokeWidth: SERIES_CIRCLE_STROKE,
-                        circleStrokeColor: Colors.ORANGE,
-                        visibility: 'visible'
-                      }}
-                    />,
-                    <MapLibreRN.SymbolLayer
-                      key="symbol_visited_must"
-                      id={series_visited.id}
-                      sourceID={series_visited.source}
-                      sourceLayerID={series_visited['source-layer']}
-                      aboveLayerID={`${series_visited.id}_must_wrapper`}
-                      filter={['all', seriesVisitedFilter, ['==', 'must', 1]] as any}
-                      minZoomLevel={series_visited.minzoom}
-                      maxZoomLevel={series_visited.maxzoom}
-                      style={{
-                        symbolSpacing: 1,
-                        iconImage: '{series_id}v',
-                        iconAllowOverlap: true,
-                        iconIgnorePlacement: true,
-                        iconSize: SERIES_ICON_SIZE,
-                        visibility: 'visible',
-                        iconColor: '#666',
-                        iconOpacity: 1,
-                        iconHaloColor: '#ffffff',
-                        iconHaloWidth: 1,
-                        iconHaloBlur: 0.5,
-                        textAnchor: 'top',
-                        textField: seriesTextField,
-                        textFont: ['Noto Sans Regular'],
-                        textMaxWidth: 9,
-                        textOffset: [0, 1.3],
-                        textPadding: 2,
-                        textSize: SERIES_TEXT_SIZE,
-                        textOptional: true,
-                        textIgnorePlacement: false,
-                        textAllowOverlap: false,
-                        textColor: '#666',
-                        textHaloColor: '#ffffff',
-                        textHaloWidth: 1,
-                        textHaloBlur: 0.5
-                      }}
-                    />
-                  ];
-                } catch (error) {
-                  console.warn('SymbolLayer render error:', error);
-                  return null;
-                }
-              })()
+              try {
+                return [
+                  <MapLibreRN.SymbolLayer
+                    key="symbol_visited_normal"
+                    id={`${series_visited.id}_normal`}
+                    sourceID={series_visited.source}
+                    sourceLayerID={series_visited['source-layer']}
+                    aboveLayerID={'waterway-name'}
+                    filter={['all', seriesVisitedFilter, ['!=', 'must', 1]] as any}
+                    minZoomLevel={series_visited.minzoom}
+                    maxZoomLevel={series_visited.maxzoom}
+                    style={{
+                      symbolSpacing: 1,
+                      iconImage: '{series_id}v',
+                      iconAllowOverlap: true,
+                      iconIgnorePlacement: true,
+                      iconSize: SERIES_ICON_SIZE,
+                      visibility: 'visible',
+                      iconColor: '#666',
+                      iconOpacity: 1,
+                      iconHaloColor: '#ffffff',
+                      iconHaloWidth: 1,
+                      iconHaloBlur: 0.5,
+                      textAnchor: 'top',
+                      textField: seriesTextField,
+                      textFont: ['Noto Sans Regular'],
+                      textMaxWidth: 9,
+                      textOffset: [0, 1.3],
+                      textPadding: 2,
+                      textSize: SERIES_TEXT_SIZE,
+                      textOptional: true,
+                      textIgnorePlacement: false,
+                      textAllowOverlap: false,
+                      textColor: '#666',
+                      textHaloColor: '#ffffff',
+                      textHaloWidth: 1,
+                      textHaloBlur: 0.5
+                    }}
+                  />,
+                  <MapLibreRN.CircleLayer
+                    key="circle_visited_must"
+                    id={`${series_visited.id}_must_wrapper`}
+                    sourceID={series_visited.source}
+                    sourceLayerID={series_visited['source-layer']}
+                    aboveLayerID={`${series_visited.id}_normal`}
+                    filter={['all', seriesVisitedFilter, ['==', 'must', 1]] as any}
+                    minZoomLevel={series_visited.minzoom}
+                    maxZoomLevel={series_visited.maxzoom}
+                    style={{
+                      circleRadius: SERIES_CIRCLE_RADIUS,
+                      circleColor: Colors.ORANGE,
+                      circleOpacity: 1,
+                      circleStrokeWidth: SERIES_CIRCLE_STROKE,
+                      circleStrokeColor: Colors.ORANGE,
+                      visibility: 'visible'
+                    }}
+                  />,
+                  <MapLibreRN.SymbolLayer
+                    key="symbol_visited_must"
+                    id={series_visited.id}
+                    sourceID={series_visited.source}
+                    sourceLayerID={series_visited['source-layer']}
+                    aboveLayerID={`${series_visited.id}_must_wrapper`}
+                    filter={['all', seriesVisitedFilter, ['==', 'must', 1]] as any}
+                    minZoomLevel={series_visited.minzoom}
+                    maxZoomLevel={series_visited.maxzoom}
+                    style={{
+                      symbolSpacing: 1,
+                      iconImage: '{series_id}v',
+                      iconAllowOverlap: true,
+                      iconIgnorePlacement: true,
+                      iconSize: SERIES_ICON_SIZE,
+                      visibility: 'visible',
+                      iconColor: '#666',
+                      iconOpacity: 1,
+                      iconHaloColor: '#ffffff',
+                      iconHaloWidth: 1,
+                      iconHaloBlur: 0.5,
+                      textAnchor: 'top',
+                      textField: seriesTextField,
+                      textFont: ['Noto Sans Regular'],
+                      textMaxWidth: 9,
+                      textOffset: [0, 1.3],
+                      textPadding: 2,
+                      textSize: SERIES_TEXT_SIZE,
+                      textOptional: true,
+                      textIgnorePlacement: false,
+                      textAllowOverlap: false,
+                      textColor: '#666',
+                      textHaloColor: '#ffffff',
+                      textHaloWidth: 1,
+                      textHaloBlur: 0.5
+                    }}
+                  />
+                ];
+              } catch (error) {
+                console.warn('SymbolLayer render error:', error);
+                return null;
+              }
+            })()
             : null}
         </MapLibreRN.VectorSource>
 

+ 33 - 6
src/screens/InAppScreens/TravelsScreen/Components/AccordionListItem.tsx

@@ -6,13 +6,15 @@ import {
   Image,
   LayoutAnimation,
   Platform,
-  UIManager
+  UIManager,
+  Share
 } from 'react-native';
 import { CheckBox } from 'src/components';
 
 import ChevronIcon from '../../../../../assets/icons/chevron-left.svg';
 import { API_HOST } from 'src/constants';
 import InfoIcon from '../../../../../assets/icons/info.svg';
+import ShareIcon from '../../../../../assets/icons/share.svg';
 
 import { styles } from './styles';
 import { Colors } from 'src/theme';
@@ -49,7 +51,9 @@ export const AccordionListItem = React.memo(
     setIsInfoModalVisible,
     setInfoItem,
     isSeries,
-    token
+    token,
+    highlightItemId,
+    onHighlightLayout
   }: {
     item: SeriesGroup;
     onCheckboxChange: (subItem: SeriesItem, groupName: string, double?: boolean) => void;
@@ -57,8 +61,11 @@ export const AccordionListItem = React.memo(
     setInfoItem: (item: SeriesItem) => void;
     isSeries: boolean;
     token: string;
+    highlightItemId?: number;
+    onHighlightLayout?: (y: number) => void;
   }) => {
-    const [isExpanded, setIsExpanded] = useState(false);
+    const shouldAutoExpand = item.items.some(subItem => subItem.item_id === highlightItemId);
+    const [isExpanded, setIsExpanded] = useState(shouldAutoExpand);
 
     const toggleExpand = () => {
       LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
@@ -69,7 +76,7 @@ export const AccordionListItem = React.memo(
       <View style={styles.sectionContainer}>
         <TouchableOpacity onPress={toggleExpand} style={styles.header}>
           <View style={styles.headerContainer}>
-            {item.icon && (
+            {!!item.icon && (
               <Image
                 source={{ uri: API_HOST + item.icon }}
                 style={[
@@ -92,7 +99,15 @@ export const AccordionListItem = React.memo(
         {isExpanded && (
           <View style={[styles.content, { backgroundColor: Colors.FILL_LIGHT }]}>
             {item.items.map((subItem, index) => (
-              <View key={index} style={styles.itemContainer}>
+              <View 
+                key={index} 
+                style={[styles.itemContainer, subItem.item_id === highlightItemId ? { backgroundColor: 'rgba(0,0,255,0.1)' } : {}]}
+                onLayout={(event) => {
+                  if (subItem.item_id === highlightItemId && onHighlightLayout) {
+                    onHighlightLayout(event.nativeEvent.layout.y);
+                  }
+                }}
+              >
                 <TouchableOpacity
                   style={styles.headerContainer}
                   onPress={() => !subItem.readonly && token && onCheckboxChange(subItem, item.name)}
@@ -115,7 +130,7 @@ export const AccordionListItem = React.memo(
                       />
                     </View>
                   )}
-                  {subItem.icon && (
+                  {!!subItem.icon && (
                     <Image
                       source={{ uri: API_HOST + subItem.icon }}
                       style={[styles.itemIcon, !token ? { marginLeft: 0 } : {}]}
@@ -141,6 +156,18 @@ export const AccordionListItem = React.memo(
                     <InfoIcon />
                   </TouchableOpacity>
                 )}
+                <TouchableOpacity
+                  style={[styles.info, { marginLeft: 0 }]}
+                  onPress={() => {
+                    Share.share(
+                      Platform.OS === 'ios'
+                        ? { url: `${API_HOST}/series-item/${subItem.item_id}` }
+                        : { message: `${API_HOST}/series-item/${subItem.item_id}` }
+                    );
+                  }}
+                >
+                  <ShareIcon width={16} height={16} fill={Colors.DARK_BLUE} />
+                </TouchableOpacity>
               </View>
             ))}
           </View>

+ 94 - 14
src/screens/InAppScreens/TravelsScreen/SeriesItemScreen/index.tsx

@@ -1,6 +1,6 @@
-import React, { useCallback, useEffect, useState } from 'react';
+import React, { useCallback, useEffect, useRef, useState } from 'react';
 import { useFocusEffect } from '@react-navigation/native';
-import { View, Text, FlatList } from 'react-native';
+import { View, Text, FlatList, Alert } from 'react-native';
 import { Button, Header, Input, Loading, Modal, PageWrapper } from 'src/components';
 
 import SearchIcon from '../../../../../assets/icons/search.svg';
@@ -44,8 +44,10 @@ interface Route {
 }
 
 export const SeriesItemScreen = ({ route }: { route: any }) => {
-  const { id, name, token } = route.params;
+  const { id, name, token, scrollToItemId } = route.params;
   const { mutate: updateSeriesItem } = usePostSetToggleItem();
+  const flatListRef = useRef<FlatList>(null);
+  const scrolledRef = useRef(false);
 
   const [search, setSearch] = useState<string>('');
   const [filteredData, setFilteredData] = useState<FilteredData>({});
@@ -140,28 +142,106 @@ export const SeriesItemScreen = ({ route }: { route: any }) => {
     setActiveFilteredData(filteredData[routes[index].key]);
   }, [filteredData]);
 
-  const renderScene = ({ route }: { route: Route }) => {
+  const [subItemY, setSubItemY] = useState<number | null>(null);
+  const [groupY, setGroupY] = useState<number | null>(null);
+
+  useEffect(() => {
+    if (!scrollToItemId || scrolledRef.current || !activeFilteredData?.length) return;
+    
+    const targetId = String(scrollToItemId);
+    const groupIndex = activeFilteredData.findIndex((group) =>
+      group.items.some((item) => String(item.item_id) === targetId)
+    );
+    
+    if (groupIndex !== -1) {
+      if (subItemY !== null && groupY !== null) {
+        scrolledRef.current = true;
+        
+        const scroll = () => {
+          flatListRef.current?.scrollToIndex({
+            index: groupIndex,
+            animated: true,
+            viewPosition: 0,
+            viewOffset: -subItemY - 60 + 50 // -subItemY to push group up, -60 for header, +50 buffer
+          });
+        };
+        
+        const timer1 = setTimeout(scroll, 300);
+        const timer2 = setTimeout(scroll, 800);
+        
+        return () => {
+          clearTimeout(timer1);
+          clearTimeout(timer2);
+        };
+      } else {
+        const roughScroll = () => {
+          flatListRef.current?.scrollToIndex({
+            index: groupIndex,
+            animated: false,
+            viewPosition: 0
+          });
+        };
+        const timer = setTimeout(roughScroll, 600);
+        return () => clearTimeout(timer);
+      }
+    } else {
+      scrolledRef.current = true;
+    }
+  }, [scrollToItemId, activeFilteredData, subItemY, groupY]);
+
+  const renderScene = ({ route: tabRoute }: { route: Route }) => {
     return isLoading ? (
       <Loading />
     ) : (
       <FlatList
+        ref={flatListRef}
         key={routes[index].key}
         keyExtractor={(item, index) => index.toString()}
         showsVerticalScrollIndicator={false}
         initialNumToRender={15}
+        windowSize={11}
         style={{ paddingTop: 10 }}
         contentContainerStyle={{ paddingBottom: 16 }}
         data={activeFilteredData}
-        renderItem={({ item }) => (
-          <AccordionListItem
-            item={item}
-            onCheckboxChange={handleCheckboxChange}
-            setIsInfoModalVisible={setIsInfoModalVisible}
-            setInfoItem={setInfoItem}
-            isSeries={id === -1}
-            token={token}
-          />
-        )}
+        onScrollToIndexFailed={(info) => {
+          const offset = info.averageItemLength * info.index;
+          flatListRef.current?.scrollToOffset({ offset, animated: false });
+          setTimeout(() => {
+            flatListRef.current?.scrollToIndex({
+              index: info.index,
+              animated: false,
+              viewPosition: 0
+            });
+          }, 150);
+        }}
+        renderItem={({ item, index }) => {
+          const targetId = String(scrollToItemId);
+          const isTargetGroup = item.items.some((subItem) => String(subItem.item_id) === targetId);
+          
+          return (
+            <View 
+              onLayout={(e) => {
+                if (isTargetGroup) {
+                  const y = e.nativeEvent.layout.y;
+                  setGroupY((prev) => (prev === null ? y : prev));
+                }
+              }}
+            >
+              <AccordionListItem
+                item={item}
+                onCheckboxChange={handleCheckboxChange}
+                setIsInfoModalVisible={setIsInfoModalVisible}
+                setInfoItem={setInfoItem}
+                isSeries={id === -1}
+                token={token}
+                highlightItemId={scrollToItemId}
+                onHighlightLayout={(y) => {
+                  setSubItemY((prev) => (prev === null ? y : prev));
+                }}
+              />
+            </View>
+          );
+        }}
       />
     );
   };

+ 95 - 0
src/screens/InAppScreens/TravelsScreen/SeriesShareScreen/index.tsx

@@ -0,0 +1,95 @@
+import React, { useEffect } from 'react';
+import { View, ActivityIndicator, StyleSheet } from 'react-native';
+import { useRoute, useNavigation } from '@react-navigation/native';
+import { useGetSingleSeriesItemMutation } from 'src/modules/api/series/queries/use-post-get-single-item';
+import { NAVIGATION_PAGES } from 'src/types';
+import { storage, StoreType } from 'src/storage';
+
+export const SeriesShareScreen = () => {
+  const route = useRoute<any>();
+  const navigation = useNavigation<any>();
+  const id = route.params?.id;
+
+  const { mutateAsync: getSingleSeriesItem } = useGetSingleSeriesItemMutation();
+
+  useEffect(() => {
+    if (!id) {
+      navigation.replace(NAVIGATION_PAGES.IN_APP);
+      return;
+    }
+
+    let isMounted = true;
+
+    const fetchAndRedirect = async () => {
+      try {
+        const itemId = parseInt(id, 10);
+        const data = await getSingleSeriesItem({ id: itemId });
+        const item = data?.data?.[0];
+
+        if (!isMounted) return;
+
+        if (!item) {
+          navigation.replace(NAVIGATION_PAGES.IN_APP);
+          return;
+        }
+
+        const token = storage.get('token', StoreType.STRING) as string;
+
+        if (item.location_geojson) {
+          navigation.replace(NAVIGATION_PAGES.IN_APP, {
+            screen: 'DrawerApp',
+            params: {
+              screen: NAVIGATION_PAGES.IN_APP_MAP_TAB,
+              params: {
+                screen: NAVIGATION_PAGES.MAP_TAB,
+                params: {
+                  seriesItemShare: { ...item, _timestamp: Date.now() }
+                }
+              }
+            }
+          });
+        } else {
+          navigation.replace(NAVIGATION_PAGES.IN_APP, {
+            screen: 'DrawerApp',
+            params: {
+              screen: NAVIGATION_PAGES.IN_APP_TRAVELS_TAB,
+              params: {
+                screen: NAVIGATION_PAGES.SERIES_ITEM,
+                params: {
+                  id: item.series_id,
+                  name: item.series_name,
+                  token,
+                  scrollToItemId: item.item_id
+                }
+              }
+            }
+          });
+        }
+      } catch (error) {
+        console.error('Error fetching series item for share:', error);
+        navigation.replace(NAVIGATION_PAGES.IN_APP);
+      }
+    };
+
+    fetchAndRedirect();
+
+    return () => {
+      isMounted = false;
+    };
+  }, [id]);
+
+  return (
+    <View style={styles.container}>
+      <ActivityIndicator size="large" color="#0F3F4F" />
+    </View>
+  );
+};
+
+const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    justifyContent: 'center',
+    alignItems: 'center',
+    backgroundColor: '#fff'
+  }
+});

+ 4 - 2
src/types/api.ts

@@ -225,7 +225,8 @@ export enum API_ENDPOINT {
   GET_USERS_WHO_TICKED_SERIES = 'get-users-who-ticked-series',
   GET_TRIPS_FOR_REGION = 'get-trips-for-region',
   GET_REGIONS_THAT_HAVE_TRIPS = 'get-regions-that-have-trips',
-  GET_MAP_DATA = 'get-map-data'
+  GET_MAP_DATA = 'get-map-data',
+  GET_SINGLE_SERIES_ITEM = 'get-single-item'
 }
 
 export enum API {
@@ -424,7 +425,8 @@ export enum API {
   GET_USERS_WHO_TICKED_SERIES = `${API_ROUTE.SERIES}/${API_ENDPOINT.GET_USERS_WHO_TICKED_SERIES}`,
   GET_TRIPS_FOR_REGION = `${API_ROUTE.TRIPS}/${API_ENDPOINT.GET_TRIPS_FOR_REGION}`,
   GET_REGIONS_THAT_HAVE_TRIPS = `${API_ROUTE.TRIPS}/${API_ENDPOINT.GET_REGIONS_THAT_HAVE_TRIPS}`,
-  GET_MAP_DATA = `${API_ROUTE.REGIONS}/${API_ENDPOINT.GET_MAP_DATA}`
+  GET_MAP_DATA = `${API_ROUTE.REGIONS}/${API_ENDPOINT.GET_MAP_DATA}`,
+  GET_SINGLE_SERIES_ITEM = `${API_ROUTE.SERIES}/${API_ENDPOINT.GET_SINGLE_SERIES_ITEM}`
 }
 
 export type BaseAxiosError = AxiosError;

+ 1 - 0
src/types/navigation.ts

@@ -57,6 +57,7 @@ export enum NAVIGATION_PAGES {
   USERS_MAP = 'inAppUsersMap',
   REGION_PREVIEW = 'inAppRegionPreview',
   USERS_LIST = 'inAppUsersList',
+  SERIES_SHARE = 'inAppSeriesShare',
   SUGGEST_SERIES = 'inAppSuggestSeries',
   FRIENDS_LIST = 'inAppFriendsList',
   MY_FRIENDS = 'inAppMyFriends',