|
@@ -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;
|