Viktoriia 1 год назад
Родитель
Сommit
997e2812d5

+ 5 - 0
Route.tsx

@@ -73,6 +73,7 @@ import {
 import RegionViewScreen from 'src/screens/InAppScreens/MapScreen/RegionViewScreen';
 import { enableScreens } from 'react-native-screens';
 import UsersListScreen from 'src/screens/InAppScreens/MapScreen/UsersListScreen';
+import SuggestSeriesScreen from 'src/screens/InAppScreens/TravelsScreen/SuggestSeriesScreen';
 
 enableScreens();
 
@@ -348,6 +349,10 @@ const Route = () => {
                     name={NAVIGATION_PAGES.USERS_MAP}
                     component={UsersMapScreen}
                   />
+                  <ScreenStack.Screen
+                    name={NAVIGATION_PAGES.SUGGEST_SERIES}
+                    component={SuggestSeriesScreen}
+                  />
                 </ScreenStack.Navigator>
               )}
             </BottomTab.Screen>

+ 2 - 0
app.config.ts

@@ -7,6 +7,7 @@ import type { ConfigContext, ExpoConfig } from 'expo/config';
 
 const API_HOST = env.ENV === 'production' ? env.PRODUCTION_API_HOST : env.DEVELOPMENT_API_HOST;
 const MAP_HOST = env.ENV === 'production' ? env.PRODUCTION_MAP_HOST : env.DEVELOPMENT_MAP_HOST;
+const GOOGLE_MAP_PLACES_APIKEY = env.GOOGLE_MAP_PLACES_APIKEY;
 
 dotenv.config({
   path: path.resolve(process.cwd(), '.env')
@@ -29,6 +30,7 @@ export default ({ config }: ConfigContext): ExpoConfig => ({
     ENV: env.ENV,
     API_HOST: API_HOST,
     MAP_HOST: MAP_HOST,
+    GOOGLE_MAP_PLACES_APIKEY: GOOGLE_MAP_PLACES_APIKEY,
     eas: {
       projectId: env.EAS_PROJECT_ID
     }

+ 9 - 0
assets/icons/travels-section/add.svg

@@ -0,0 +1,9 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M10.9254 17.0986V13.3909H7.21756C6.44705 13.3909 5.82715 12.7709 5.82715 12.0004C5.82715 11.2299 6.44705 10.6099 7.21756 10.6099H10.9254V6.90219C10.9254 6.13164 11.5453 5.51172 12.3158 5.51172C13.0863 5.51172 13.7062 6.13164 13.7062 6.90219V10.6099H17.4141C18.1846 10.6099 18.8045 11.2299 18.8045 12.0004C18.8045 12.7709 18.1846 13.3909 17.4141 13.3909H13.7062V17.0986C13.7062 17.8692 13.0863 18.4891 12.3158 18.4891C11.5453 18.4891 10.9254 17.8692 10.9254 17.0986Z" fill="#0F3F4F"/>
+<mask id="mask0_3383_31447" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="-1" width="25" height="25">
+<path d="M24.3707 24H0.26123V-0.109467H24.3707V24Z" fill="white"/>
+</mask>
+<g mask="url(#mask0_3383_31447)">
+<path d="M12.3158 24C5.87582 24 0.631592 18.6141 0.631592 12C0.631592 5.38595 5.87582 0 12.3158 0C18.7558 0 24 5.38596 24 12C24 18.6141 18.7558 24 12.3158 24ZM12.3158 21.7193C17.5558 21.7193 21.7795 17.3815 21.7795 12C21.7795 6.61849 17.5558 2.28063 12.3158 2.28063C7.07593 2.28063 2.85223 6.6185 2.85223 12C2.85223 17.3815 7.07593 21.7193 12.3158 21.7193Z" fill="#0F3F4F"/>
+</g>
+</svg>

+ 1 - 0
package.json

@@ -51,6 +51,7 @@
     "react-native-calendars": "^1.1304.1",
     "react-native-device-detection": "^0.2.1",
     "react-native-gesture-handler": "~2.12.0",
+    "react-native-google-places-autocomplete": "^2.5.6",
     "react-native-image-viewing": "^0.2.2",
     "react-native-keyboard-aware-scroll-view": "^0.9.5",
     "react-native-map-clustering": "^3.4.2",

+ 3 - 0
src/constants/secrets.ts

@@ -14,3 +14,6 @@ export const setFastestMapHost = (server: string | null) => {
 };
 
 export const APP_VERSION = Constants?.expoConfig?.version ?? Constants?.manifest?.version;
+
+export const GOOGLE_MAP_PLACES_APIKEY =
+  extra?.GOOGLE_MAP_PLACES_APIKEY || Constants?.expoConfig?.extra?.GOOGLE_MAP_PLACES_APIKEY;

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

@@ -5,3 +5,6 @@ export * from './use-post-get-items-for-series';
 export * from './use-post-get-toggle-item';
 export * from './use-post-get-series-groups-ranking';
 export * from './use-post-get-series-ranking';
+export * from './use-post-get-data-from-point';
+export * from './use-post-get-suggestion-data';
+export * from './use-post-submit-suggestion';

+ 17 - 0
src/modules/api/series/queries/use-post-get-data-from-point.tsx

@@ -0,0 +1,17 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { seriesQueryKeys } from '../series-query-keys';
+import { seriesApi, type PostGetDataFromPoint } from '../series-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const useGetDataFromPoint = (token: string, lat: number, lng: number, enabled: boolean) => {
+  return useQuery<PostGetDataFromPoint, BaseAxiosError>({
+    queryKey: seriesQueryKeys.getDataFromPoint(token, lat, lng),
+    queryFn: async () => {
+      const response = await seriesApi.getDataFromPoint(token, lat, lng);
+      return response.data;
+    },
+    enabled
+  });
+};

+ 16 - 0
src/modules/api/series/queries/use-post-get-suggestion-data.tsx

@@ -0,0 +1,16 @@
+import { useQuery } from '@tanstack/react-query';
+
+import { seriesQueryKeys } from '../series-query-keys';
+import { seriesApi, type PostGetSuggestionData } from '../series-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const useGetSuggestionData = () => {
+  return useQuery<PostGetSuggestionData, BaseAxiosError>({
+    queryKey: seriesQueryKeys.getSuggestionData(),
+    queryFn: async () => {
+      const response = await seriesApi.getSuggestionData();
+      return response.data;
+    }
+  });
+};

+ 21 - 0
src/modules/api/series/queries/use-post-submit-suggestion.tsx

@@ -0,0 +1,21 @@
+import { useMutation } from '@tanstack/react-query';
+
+import { seriesQueryKeys } from '../series-query-keys';
+import { seriesApi, SubmitSuggestionTypes, SubmitSuggestionReturn } from '../series-api';
+
+import type { BaseAxiosError } from '../../../../types';
+
+export const useSubmitSuggestionMutation = () => {
+  return useMutation<
+    SubmitSuggestionReturn,
+    BaseAxiosError,
+    SubmitSuggestionTypes,
+    SubmitSuggestionReturn
+  >({
+    mutationKey: seriesQueryKeys.submitSuggestion(),
+    mutationFn: async (data) => {
+      const response = await seriesApi.submitSuggestion(data);
+      return response.data;
+    }
+  });
+};

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

@@ -107,6 +107,45 @@ export interface PostGetSeriesRanking extends ResponseType {
   data: RankingData;
 }
 
+export interface PostGetDataFromPoint extends ResponseType {
+  nm: { id: number };
+  dare: { id: number };
+  country: string;
+}
+
+export interface PostGetSuggestionData extends ResponseType {
+  nm: {
+    id: number;
+    region_name: string;
+  }[];
+  dare: {
+    id: number;
+    name: string;
+  }[];
+  series: {
+    id: number;
+    name: string;
+  }[];
+  grouped: {
+    [key: string]: { id: number; name: string; group_name: string | null }[];
+  };
+}
+
+export interface SubmitSuggestionReturn extends ResponseType {}
+
+export type SubmitSuggestionTypes = {
+  token: string;
+  comment: string;
+  nm: number;
+  dare: number;
+  series: number;
+  name: string;
+  link: string;
+  lat: number;
+  lng: number;
+  item: number;
+};
+
 export const seriesApi = {
   getSeries: (token: string | null, regions: string) =>
     request.postForm<PostGetSeries>(API.SERIES, { token, regions }),
@@ -136,5 +175,10 @@ export const seriesApi = {
       id,
       page,
       page_size
-    })
+    }),
+  getDataFromPoint: (token: string, lat: number, lng: number) =>
+    request.postForm<PostGetDataFromPoint>(API.GET_DATA_FROM_POINT, { token, lat, lng }),
+  getSuggestionData: () => request.postForm<PostGetSuggestionData>(API.GET_SUGGESTION_DATA),
+  submitSuggestion: (data: SubmitSuggestionTypes) =>
+    request.postForm<SubmitSuggestionReturn>(API.SUBMIT_SUGGESTION, data)
 };

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

@@ -5,5 +5,8 @@ export const seriesQueryKeys = {
   getItemsForSeries: (token: string, series_id: string) => ['getItemsForSeries', {token, series_id}] as const,
   setToggleItem: () => ['setToggleItem'] as const,
   getSeriesGroupsRanking: () => ['getSeriesGroupsRanking'] as const,
-  getSeriesRanking: (id: number, page: number, page_size: number) => ['getSeriesRanking', {id, page, page_size}] as const
+  getSeriesRanking: (id: number, page: number, page_size: number) => ['getSeriesRanking', {id, page, page_size}] as const,
+  getDataFromPoint: (token: string, lat: number, lng: number) => ['getDataFromPoint', {token, lat, lng}] as const,
+  getSuggestionData: () => ['getSuggestionData'] as const,
+  submitSuggestion: () => ['submitSuggestion'] as const
 };

+ 25 - 2
src/screens/InAppScreens/TravelsScreen/Series/index.tsx

@@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react';
 import { View, Text, FlatList, Image, TouchableOpacity } from 'react-native';
 import { useNavigation } from '@react-navigation/native';
 
-import { Header, PageWrapper, HorizontalTabView, Loading } from 'src/components';
+import { Header, PageWrapper, HorizontalTabView, Loading, WarningModal } from 'src/components';
 import { useGetSeriesGroups, useGetSeriesWithGroup } from '@api/series';
 import { useFocusEffect } from '@react-navigation/native';
 
@@ -10,6 +10,7 @@ import { API_HOST } from 'src/constants';
 import { StoreType, storage } from 'src/storage';
 import { styles } from './styles';
 import { NAVIGATION_PAGES } from 'src/types';
+import AddSvg from 'assets/icons/travels-section/add.svg';
 
 interface SeriesGroup {
   key: string;
@@ -28,9 +29,11 @@ interface SeriesList {
 }
 
 const SeriesScreen = () => {
+  const token = storage.get('token', StoreType.STRING) as string;
   const [isLoading, setIsLoading] = useState(true);
   const [index, setIndex] = useState(0);
   const [routes, setRoutes] = useState<SeriesGroup[]>([]);
+  const [warningVisible, setWarningVisible] = useState(false);
   const { data } = useGetSeriesGroups(true);
   const navigation = useNavigation();
 
@@ -72,7 +75,22 @@ const SeriesScreen = () => {
 
   return (
     <PageWrapper>
-      <Header label={'Series'} />
+      <Header
+        label={'Series'}
+        rightElement={
+          <TouchableOpacity
+            onPress={() => {
+              if (token) {
+                navigation.navigate(NAVIGATION_PAGES.SUGGEST_SERIES as never);
+              } else {
+                setWarningVisible(true);
+              }
+            }}
+          >
+            <AddSvg />
+          </TouchableOpacity>
+        }
+      />
       <HorizontalTabView
         index={index}
         setIndex={setIndex}
@@ -81,6 +99,11 @@ const SeriesScreen = () => {
           <SeriesList groupId={route.key} navigation={navigation} />
         )}
       />
+      <WarningModal
+        type="unauthorized"
+        isVisible={warningVisible}
+        onClose={() => setWarningVisible(false)}
+      />
     </PageWrapper>
   );
 };

+ 40 - 0
src/screens/InAppScreens/TravelsScreen/SuggestSeriesScreen/SeriesSelector/index.tsx

@@ -0,0 +1,40 @@
+import React from 'react';
+import { TouchableOpacity, View, Text } from 'react-native';
+
+import { styles } from './styles';
+import { Colors } from 'src/theme';
+
+import ChevronIcon from 'assets/icons/travels-screens/down-arrow.svg';
+
+const SeriesSelector = ({
+  selectedSeries,
+  setSeriesVisible,
+  props
+}: {
+  selectedSeries: {
+    id: number;
+    name: string;
+    group_name: string | null;
+  } | null;
+  setSeriesVisible: (visible: boolean) => void;
+  props: any;
+}) => (
+  <View>
+    <Text style={styles.text}>Series</Text>
+    <TouchableOpacity
+      style={[
+        styles.seriesSelector,
+        props.touched.series && !selectedSeries ? { borderColor: Colors.RED, borderWidth: 1 } : {}
+      ]}
+      onPress={() => setSeriesVisible(true)}
+    >
+      <Text style={styles.container}>{selectedSeries ? selectedSeries.name : 'Select series'}</Text>
+      <ChevronIcon width={18} height={18} />
+    </TouchableOpacity>
+    {props.touched.series && !selectedSeries ? (
+      <Text style={styles.textError}>series is required</Text>
+    ) : null}
+  </View>
+);
+
+export default SeriesSelector;

+ 30 - 0
src/screens/InAppScreens/TravelsScreen/SuggestSeriesScreen/SeriesSelector/styles.tsx

@@ -0,0 +1,30 @@
+import { StyleSheet } from 'react-native';
+import { Colors } from 'src/theme';
+import { getFontSize } from 'src/utils';
+
+export const styles = StyleSheet.create({
+  container: {
+    flex: 1
+  },
+  text: {
+    color: Colors.DARK_BLUE,
+    fontSize: getFontSize(14),
+    fontFamily: 'redhat-700',
+    marginBottom: 5
+  },
+  seriesSelector: {
+    flexDirection: 'row',
+    alignItems: 'center',
+    paddingHorizontal: 10,
+    height: 40,
+    borderRadius: 6,
+    backgroundColor: Colors.FILL_LIGHT,
+    justifyContent: 'space-between'
+  },
+  textError: {
+    color: Colors.RED,
+    fontSize: getFontSize(12),
+    fontFamily: 'redhat-600',
+    marginTop: 5
+  }
+});

+ 408 - 0
src/screens/InAppScreens/TravelsScreen/SuggestSeriesScreen/index.tsx

@@ -0,0 +1,408 @@
+import React, { useEffect, useRef, useState } from 'react';
+import {
+  SafeAreaView,
+  View,
+  Text,
+  Platform,
+  TouchableOpacity,
+  StatusBar,
+  KeyboardAvoidingView,
+  TouchableWithoutFeedback,
+  Keyboard,
+  ScrollView
+} from 'react-native';
+import MapView, { Marker } from 'react-native-maps';
+import ReactModal from 'react-native-modal';
+import axios from 'axios';
+import {
+  GooglePlaceData,
+  GooglePlaceDetail,
+  GooglePlacesAutocomplete
+} from 'react-native-google-places-autocomplete';
+import { FlashList } from '@shopify/flash-list';
+import { Formik } from 'formik';
+import * as yup from 'yup';
+
+import { styles } from './styles';
+import { Header, Input, Button, WarningModal } from 'src/components';
+import { GOOGLE_MAP_PLACES_APIKEY } from 'src/constants';
+import { Colors } from 'src/theme';
+import {
+  SubmitSuggestionTypes,
+  useGetDataFromPoint,
+  useGetSuggestionData,
+  useSubmitSuggestionMutation
+} from '@api/series';
+import { StoreType, storage } from 'src/storage';
+import { ButtonVariants } from 'src/types/components';
+import SeriesSelector from './SeriesSelector';
+
+import SearchIcon from 'assets/icons/search.svg';
+
+interface Series {
+  id: number;
+  name: string;
+  group_name: string | null;
+}
+
+const SuggestionSchema = yup.object({
+  comment: yup.string().required('comment is required')
+});
+
+const SuggestSeriesScreen = ({ navigation }: { navigation: any }) => {
+  const token = storage.get('token', StoreType.STRING) as string;
+  const [isModalVisible, setIsModalVisible] = useState(false);
+  const [seriesVisible, setSeriesVisible] = useState(false);
+  const [marker, setMarker] = useState<any>(null);
+  const [coordinates, setCoordinates] = useState<any>(null);
+  const [groupedSeries, setGroupedSeries] = useState<any>(null);
+  const [region, setRegion] = useState<any>({ nmRegion: null, dareRegion: null });
+  const [selectedSeries, setSelectedSeries] = useState<Series | null>(null);
+  const [submitedModalVisible, setSubmitedModalVisible] = useState(false);
+  const [keyboardVisible, setKeyboardVisible] = useState(false);
+
+  const { data: suggestionData } = useGetSuggestionData();
+  const { data } = useGetDataFromPoint(
+    token,
+    coordinates?.lat,
+    coordinates?.lng,
+    coordinates ? true : false
+  );
+  const { mutateAsync: submitSuggestion } = useSubmitSuggestionMutation();
+
+  const mapRef = useRef<MapView>(null);
+
+  useEffect(() => {
+    const keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', () => {
+      setKeyboardVisible(true);
+    });
+    const keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => {
+      setKeyboardVisible(false);
+    });
+
+    return () => {
+      keyboardDidHideListener.remove();
+      keyboardDidShowListener.remove();
+    };
+  }, []);
+
+  useEffect(() => {
+    if (data && data.result === 'OK') {
+      const nmRegion = data.nm ? suggestionData?.nm.find((item) => item.id === data.nm.id) : null;
+      const dareRegion = data.dare
+        ? suggestionData?.dare.find((item) => item.id === data.dare.id)
+        : null;
+      setRegion({ nmRegion, dareRegion });
+      setSelectedSeries(null);
+      setIsModalVisible(true);
+    }
+  }, [data]);
+
+  useEffect(() => {
+    if (suggestionData && suggestionData.result === 'OK') {
+      const groupedData = Object.keys(suggestionData.grouped).map((key) => ({
+        title: key,
+        data: suggestionData.grouped[key]
+      }));
+      setGroupedSeries(groupedData);
+    }
+  }, [suggestionData]);
+
+  const findPlace = async (placeId: string) => {
+    const response = await axios.get(
+      `https://maps.googleapis.com/maps/api/place/details/json?place_id=${placeId}&key=${GOOGLE_MAP_PLACES_APIKEY}`
+    );
+    return { url: response.data.result.url, name: response.data.result.name };
+  };
+
+  const animateMapToRegion = (latitude: number, longitude: number) => {
+    const region = {
+      latitude,
+      longitude,
+      latitudeDelta: 0.015,
+      longitudeDelta: 0.0121
+    };
+    mapRef.current?.animateToRegion(region, 500);
+  };
+
+  const handlePoiClick = async (event: any) => {
+    const { placeId, coordinate } = event.nativeEvent;
+    const { url, name } = await findPlace(placeId);
+    setMarker({
+      placeId,
+      name,
+      coordinate,
+      url
+    });
+    setCoordinates({ lat: coordinate.latitude, lng: coordinate.longitude });
+    animateMapToRegion(coordinate.latitude, coordinate.longitude);
+  };
+
+  const handlePlaceSelection = (data: GooglePlaceData, details: GooglePlaceDetail | null) => {
+    if (details) {
+      const { geometry } = details;
+
+      setMarker({
+        placeId: data.place_id,
+        name: data.structured_formatting.main_text,
+        coordinate: {
+          latitude: geometry.location.lat,
+          longitude: geometry.location.lng
+        },
+        url: details.url
+      });
+      setCoordinates({ lat: geometry.location.lat, lng: geometry.location.lng });
+      animateMapToRegion(geometry.location.lat, geometry.location.lng);
+    }
+  };
+
+  const renderGroup = ({ item }: { item: { title: string; data: Series[] } }) => {
+    return (
+      <View>
+        {item.title !== '-' && <Text style={styles.groupTitle}>{item.title}</Text>}
+        {item.data.map((series: Series) => (
+          <TouchableOpacity
+            key={series.id}
+            style={styles.seriesItem}
+            onPress={() => {
+              setSelectedSeries(series);
+              setSeriesVisible(false);
+            }}
+          >
+            <Text style={styles.seriesText}>{series.name}</Text>
+          </TouchableOpacity>
+        ))}
+      </View>
+    );
+  };
+
+  const handleClose = () => {
+    setSubmitedModalVisible(false);
+    setIsModalVisible(false);
+    navigation.goBack();
+  };
+
+  return (
+    <SafeAreaView
+      style={{
+        height: '100%',
+        paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0
+      }}
+    >
+      <View style={styles.wrapper}>
+        <Header label={'Suggest new Series item'} />
+        <View style={styles.searchContainer}>
+          <GooglePlacesAutocomplete
+            placeholder="Add a landmark"
+            onPress={handlePlaceSelection}
+            query={{
+              key: GOOGLE_MAP_PLACES_APIKEY,
+              language: 'en',
+              types: 'establishment'
+            }}
+            nearbyPlacesAPI="GooglePlacesSearch"
+            fetchDetails={true}
+            styles={{
+              textInput: styles.searchInput
+            }}
+            isRowScrollable={true}
+            renderLeftButton={() => (
+              <View style={styles.searchIcon}>
+                <SearchIcon fill={Colors.LIGHT_GRAY} />
+              </View>
+            )}
+          />
+        </View>
+      </View>
+
+      <View style={styles.container}>
+        <MapView
+          ref={mapRef}
+          style={styles.map}
+          showsMyLocationButton={true}
+          showsUserLocation={true}
+          showsCompass={false}
+          zoomControlEnabled={false}
+          mapType={'standard'}
+          maxZoomLevel={18}
+          minZoomLevel={0}
+          initialRegion={{
+            latitude: 0,
+            longitude: 0,
+            latitudeDelta: 180,
+            longitudeDelta: 180
+          }}
+          provider="google"
+          onPoiClick={handlePoiClick}
+        >
+          {marker && (
+            <Marker coordinate={marker.coordinate} onPress={() => setIsModalVisible(true)} />
+          )}
+        </MapView>
+      </View>
+
+      <ReactModal
+        isVisible={isModalVisible}
+        onBackdropPress={() => setIsModalVisible(false)}
+        style={styles.modal}
+        statusBarTranslucent={true}
+        presentationStyle="overFullScreen"
+      >
+        <Formik
+          initialValues={{
+            comment: '',
+            nm: region.nmRegion ? region.nmRegion.region_name : '-',
+            dare: region.dareRegion ? region.dareRegion.name : '-',
+            series: selectedSeries ? selectedSeries.id : null,
+            name: marker?.name,
+            link: marker?.url,
+            lat: coordinates?.lat,
+            lng: coordinates?.lng,
+            item: -1
+          }}
+          validationSchema={SuggestionSchema}
+          onSubmit={(values) => {
+            const { comment, name, link, lat, lng, item } = values;
+            if (!selectedSeries) return;
+
+            const submitData: SubmitSuggestionTypes = {
+              token,
+              comment,
+              name,
+              link,
+              lat,
+              lng,
+              item,
+              nm: region.nmRegion.id,
+              dare: region.dareRegion ? region.dareRegion.id : 0,
+              series: selectedSeries.id
+            };
+
+            submitSuggestion(submitData, {
+              onSuccess: () => {
+                setSubmitedModalVisible(true);
+              }
+            });
+          }}
+        >
+          {(props) => (
+            <KeyboardAvoidingView
+              behavior={'padding'}
+              style={[styles.modalContent, { maxHeight: keyboardVisible ? undefined : '90%' }]}
+            >
+              <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
+                <ScrollView
+                  contentContainerStyle={{ gap: 16 }}
+                  showsVerticalScrollIndicator={false}
+                  keyboardShouldPersistTaps="handled"
+                >
+                  <Input
+                    placeholder="NM region"
+                    value={props.values.nm}
+                    editable={false}
+                    header="NM region"
+                    height={40}
+                  />
+                  <Input
+                    placeholder="DARE place"
+                    value={props.values.dare}
+                    editable={false}
+                    header="DARE place"
+                    height={40}
+                  />
+                  <SeriesSelector
+                    selectedSeries={selectedSeries}
+                    setSeriesVisible={setSeriesVisible}
+                    props={props}
+                  />
+                  <Input
+                    placeholder="Name"
+                    value={props.values.name}
+                    editable={false}
+                    header="Name"
+                    height={40}
+                  />
+                  <Input
+                    placeholder="URL"
+                    value={props.values.link}
+                    editable={false}
+                    header="Google maps link"
+                    height={40}
+                  />
+                  <Input
+                    multiline={true}
+                    header="Comment"
+                    value={props.values.comment}
+                    onChange={props.handleChange('comment')}
+                    formikError={props.touched.comment && props.errors.comment}
+                  />
+
+                  <View style={{ paddingBottom: 24, gap: 16 }}>
+                    <Button children="Send" onPress={props.handleSubmit} />
+                    <Button
+                      children="Close"
+                      onPress={() => setIsModalVisible(false)}
+                      variant={ButtonVariants.OPACITY}
+                      containerStyles={styles.closeBtn}
+                      textStyles={{ color: Colors.DARK_BLUE }}
+                    />
+                  </View>
+                </ScrollView>
+              </TouchableWithoutFeedback>
+            </KeyboardAvoidingView>
+          )}
+        </Formik>
+        <ReactModal
+          isVisible={seriesVisible}
+          onBackdropPress={() => setSeriesVisible(false)}
+          style={styles.modal}
+          statusBarTranslucent={true}
+          presentationStyle="overFullScreen"
+        >
+          <View style={styles.modalWrapper}>
+            <ScrollView
+              style={{ paddingBottom: 16 }}
+              showsVerticalScrollIndicator={false}
+              nestedScrollEnabled={true}
+            >
+              {groupedSeries ? (
+                <View style={{ minHeight: 100, paddingBottom: 16, paddingTop: 8 }}>
+                  <TouchableOpacity
+                    style={styles.seriesItem}
+                    onPress={() => {
+                      setSelectedSeries(null);
+                      setSeriesVisible(false);
+                    }}
+                  >
+                    <Text style={styles.seriesText}>Select series</Text>
+                  </TouchableOpacity>
+                  <FlashList
+                    viewabilityConfig={{
+                      waitForInteraction: true,
+                      itemVisiblePercentThreshold: 50,
+                      minimumViewTime: 1000
+                    }}
+                    estimatedItemSize={40}
+                    data={groupedSeries}
+                    renderItem={renderGroup}
+                    keyExtractor={(item) => item.title}
+                    nestedScrollEnabled={true}
+                  />
+                </View>
+              ) : null}
+            </ScrollView>
+          </View>
+        </ReactModal>
+        <WarningModal
+          isVisible={submitedModalVisible}
+          onClose={handleClose}
+          type="success"
+          message="Thank you for your suggestion!"
+          title="Success!"
+        />
+      </ReactModal>
+    </SafeAreaView>
+  );
+};
+
+export default SuggestSeriesScreen;

+ 72 - 0
src/screens/InAppScreens/TravelsScreen/SuggestSeriesScreen/styles.tsx

@@ -0,0 +1,72 @@
+import { StyleSheet } from 'react-native';
+import { Colors } from 'src/theme';
+
+export const styles = StyleSheet.create({
+  container: {
+    flex: 1,
+    justifyContent: 'center'
+  },
+  wrapper: {
+    marginLeft: '5%',
+    marginRight: '5%',
+    alignItems: 'center'
+  },
+  modalWrapper: {
+    backgroundColor: Colors.WHITE,
+    padding: 16,
+    borderTopLeftRadius: 10,
+    borderTopRightRadius: 10,
+    height: '86%'
+  },
+  map: {
+    ...StyleSheet.absoluteFillObject
+  },
+  searchContainer: {
+    gap: 16,
+    flexDirection: 'row',
+    alignItems: 'center',
+    marginBottom: 8
+  },
+  closeBtn: { backgroundColor: Colors.WHITE, borderColor: '#B7C6CB' },
+  modal: {
+    justifyContent: 'flex-end',
+    margin: 0
+  },
+  modalContent: {
+    backgroundColor: 'white',
+    borderRadius: 15,
+    paddingHorizontal: 16,
+    paddingVertical: 24
+  },
+  groupTitle: {
+    fontSize: 15,
+    fontFamily: 'montserrat-700',
+    color: Colors.DARK_BLUE,
+    marginTop: 16,
+    marginBottom: 8
+  },
+  seriesItem: {
+    padding: 8
+  },
+  seriesText: {
+    fontSize: 14,
+    color: Colors.DARK_BLUE,
+    fontWeight: '500'
+  },
+  searchInput: {
+    backgroundColor: Colors.FILL_LIGHT,
+    borderRadius: 0,
+    borderTopRightRadius: 4,
+    borderBottomRightRadius: 4,
+    height: 36
+  },
+  searchIcon: {
+    height: 36,
+    backgroundColor: Colors.FILL_LIGHT,
+    paddingLeft: 16,
+    alignItems: 'center',
+    justifyContent: 'center',
+    borderTopLeftRadius: 4,
+    borderBottomLeftRadius: 4
+  }
+});

+ 6 - 0
src/types/api.ts

@@ -91,6 +91,9 @@ export enum API_ENDPOINT {
   GET_USERS_FROM_REGION = 'get-users-from-region',
   GET_USERS_WHO_VISITED_REGION = 'get-users-who-visited-region',
   GET_USERS_WHO_VISITED_DARE = 'get-users-who-visited-dare',
+  GET_DATA_FROM_POINT = 'get-data-from-point',
+  GET_SUGGESTION_DATA = 'get-suggestion-data',
+  SUBMIT_SUGGESTION = 'submit-suggestion'
 }
 
 export enum API {
@@ -164,6 +167,9 @@ export enum API {
   GET_USERS_FROM_REGION = `${API_ROUTE.REGIONS}/${API_ENDPOINT.GET_USERS_FROM_REGION}`,
   GET_USERS_WHO_VISITED_REGION = `${API_ROUTE.REGIONS}/${API_ENDPOINT.GET_USERS_WHO_VISITED_REGION}`,
   GET_USERS_WHO_VISITED_DARE = `${API_ROUTE.REGIONS}/${API_ENDPOINT.GET_USERS_WHO_VISITED_DARE}`,
+  GET_DATA_FROM_POINT = `${API_ROUTE.SERIES}/${API_ENDPOINT.GET_DATA_FROM_POINT}`,
+  GET_SUGGESTION_DATA = `${API_ROUTE.SERIES}/${API_ENDPOINT.GET_SUGGESTION_DATA}`,
+  SUBMIT_SUGGESTION = `${API_ROUTE.SERIES}/${API_ENDPOINT.SUBMIT_SUGGESTION}`
 }
 
 export type BaseAxiosError = AxiosError;