index.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406
  1. import React, { useEffect, useRef, useState } from 'react';
  2. import {
  3. View,
  4. Text,
  5. TouchableOpacity,
  6. KeyboardAvoidingView,
  7. TouchableWithoutFeedback,
  8. Keyboard,
  9. ScrollView
  10. } from 'react-native';
  11. import { SafeAreaView } from 'react-native-safe-area-context';
  12. import MapView, { Marker } from 'react-native-maps';
  13. import ReactModal from 'react-native-modal';
  14. import axios from 'axios';
  15. import "react-native-get-random-values";
  16. import {
  17. GooglePlaceData,
  18. GooglePlaceDetail,
  19. GooglePlacesAutocomplete
  20. } from 'react-native-google-places-autocomplete';
  21. import { FlashList } from '@shopify/flash-list';
  22. import { Formik } from 'formik';
  23. import * as yup from 'yup';
  24. import { styles } from './styles';
  25. import { Header, Input, Button, WarningModal } from 'src/components';
  26. import { GOOGLE_MAP_PLACES_APIKEY } from 'src/constants';
  27. import { Colors } from 'src/theme';
  28. import {
  29. SubmitSuggestionTypes,
  30. useGetDataFromPoint,
  31. useGetSuggestionData,
  32. useSubmitSuggestionMutation
  33. } from '@api/series';
  34. import { StoreType, storage } from 'src/storage';
  35. import { ButtonVariants } from 'src/types/components';
  36. import SeriesSelector from './SeriesSelector';
  37. import SearchIcon from 'assets/icons/search.svg';
  38. interface Series {
  39. id: number;
  40. name: string;
  41. group_name: string | null;
  42. }
  43. const SuggestionSchema = yup.object({
  44. comment: yup.string().required('comment is required')
  45. });
  46. const SuggestSeriesScreen = ({ navigation }: { navigation: any }) => {
  47. const token = storage.get('token', StoreType.STRING) as string;
  48. const [isModalVisible, setIsModalVisible] = useState(false);
  49. const [seriesVisible, setSeriesVisible] = useState(false);
  50. const [marker, setMarker] = useState<any>(null);
  51. const [coordinates, setCoordinates] = useState<any>(null);
  52. const [groupedSeries, setGroupedSeries] = useState<any>(null);
  53. const [region, setRegion] = useState<any>({ nmRegion: null, dareRegion: null });
  54. const [selectedSeries, setSelectedSeries] = useState<Series | null>(null);
  55. const [submitedModalVisible, setSubmitedModalVisible] = useState(false);
  56. const [keyboardVisible, setKeyboardVisible] = useState(false);
  57. const { data: suggestionData } = useGetSuggestionData();
  58. const { data } = useGetDataFromPoint(
  59. token,
  60. coordinates?.lat,
  61. coordinates?.lng,
  62. coordinates ? true : false
  63. );
  64. const { mutateAsync: submitSuggestion } = useSubmitSuggestionMutation();
  65. const mapRef = useRef<MapView>(null);
  66. useEffect(() => {
  67. const keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', () => {
  68. setKeyboardVisible(true);
  69. });
  70. const keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', () => {
  71. setKeyboardVisible(false);
  72. });
  73. return () => {
  74. keyboardDidHideListener.remove();
  75. keyboardDidShowListener.remove();
  76. };
  77. }, []);
  78. useEffect(() => {
  79. if (data && data.result === 'OK') {
  80. const nmRegion = data.nm ? suggestionData?.nm.find((item) => item.id === data.nm.id) : null;
  81. const dareRegion = data.dare
  82. ? suggestionData?.dare.find((item) => item.id === data.dare.id)
  83. : null;
  84. setRegion({ nmRegion, dareRegion });
  85. setSelectedSeries(null);
  86. setIsModalVisible(true);
  87. }
  88. }, [data]);
  89. useEffect(() => {
  90. if (suggestionData && suggestionData.result === 'OK') {
  91. const groupedData = Object.keys(suggestionData.grouped).map((key) => ({
  92. title: key,
  93. data: suggestionData.grouped[key]
  94. }));
  95. setGroupedSeries(groupedData);
  96. }
  97. }, [suggestionData]);
  98. const findPlace = async (placeId: string) => {
  99. const response = await axios.get(
  100. `https://maps.googleapis.com/maps/api/place/details/json?place_id=${placeId}&key=${GOOGLE_MAP_PLACES_APIKEY}`
  101. );
  102. return { url: response.data.result.url, name: response.data.result.name };
  103. };
  104. const animateMapToRegion = (latitude: number, longitude: number) => {
  105. const region = {
  106. latitude,
  107. longitude,
  108. latitudeDelta: 0.015,
  109. longitudeDelta: 0.0121
  110. };
  111. mapRef.current?.animateToRegion(region, 500);
  112. };
  113. const handlePoiClick = async (event: any) => {
  114. const { placeId, coordinate } = event.nativeEvent;
  115. const { url, name } = await findPlace(placeId);
  116. setMarker({
  117. placeId,
  118. name,
  119. coordinate,
  120. url
  121. });
  122. setCoordinates({ lat: coordinate.latitude, lng: coordinate.longitude });
  123. animateMapToRegion(coordinate.latitude, coordinate.longitude);
  124. };
  125. const handlePlaceSelection = (data: GooglePlaceData, details: GooglePlaceDetail | null) => {
  126. if (details) {
  127. const { geometry } = details;
  128. setMarker({
  129. placeId: data.place_id,
  130. name: data.structured_formatting.main_text,
  131. coordinate: {
  132. latitude: geometry.location.lat,
  133. longitude: geometry.location.lng
  134. },
  135. url: details.url
  136. });
  137. setCoordinates({ lat: geometry.location.lat, lng: geometry.location.lng });
  138. animateMapToRegion(geometry.location.lat, geometry.location.lng);
  139. }
  140. };
  141. const renderGroup = ({ item }: { item: { title: string; data: Series[] } }) => {
  142. return (
  143. <View>
  144. {item.title !== '-' && <Text style={styles.groupTitle}>{item.title}</Text>}
  145. {item.data.map((series: Series) => (
  146. <TouchableOpacity
  147. key={series.id}
  148. style={styles.seriesItem}
  149. onPress={() => {
  150. setSelectedSeries(series);
  151. setSeriesVisible(false);
  152. }}
  153. >
  154. <Text style={styles.seriesText}>{series.name}</Text>
  155. </TouchableOpacity>
  156. ))}
  157. </View>
  158. );
  159. };
  160. const handleClose = () => {
  161. setSubmitedModalVisible(false);
  162. setIsModalVisible(false);
  163. navigation.goBack();
  164. };
  165. return (
  166. <SafeAreaView
  167. style={{
  168. height: '100%'
  169. }}
  170. >
  171. <View style={styles.wrapper}>
  172. <Header label={'Suggest new Series item'} />
  173. <View style={styles.searchContainer}>
  174. <GooglePlacesAutocomplete
  175. placeholder="Add a landmark"
  176. onPress={handlePlaceSelection}
  177. query={{
  178. key: GOOGLE_MAP_PLACES_APIKEY,
  179. language: 'en',
  180. types: 'establishment'
  181. }}
  182. nearbyPlacesAPI="GooglePlacesSearch"
  183. fetchDetails={true}
  184. styles={{
  185. textInput: styles.searchInput
  186. }}
  187. isRowScrollable={true}
  188. renderLeftButton={() => (
  189. <View style={styles.searchIcon}>
  190. <SearchIcon fill={Colors.LIGHT_GRAY} />
  191. </View>
  192. )}
  193. />
  194. </View>
  195. </View>
  196. <View style={styles.container}>
  197. <MapView
  198. ref={mapRef}
  199. style={styles.map}
  200. showsMyLocationButton={true}
  201. showsUserLocation={true}
  202. showsCompass={false}
  203. zoomControlEnabled={false}
  204. mapType={'standard'}
  205. maxZoomLevel={18}
  206. minZoomLevel={0}
  207. initialRegion={{
  208. latitude: 0,
  209. longitude: 0,
  210. latitudeDelta: 180,
  211. longitudeDelta: 180
  212. }}
  213. provider="google"
  214. onPoiClick={handlePoiClick}
  215. >
  216. {marker && (
  217. <Marker coordinate={marker.coordinate} onPress={() => setIsModalVisible(true)} />
  218. )}
  219. </MapView>
  220. </View>
  221. <ReactModal
  222. isVisible={isModalVisible}
  223. onBackdropPress={() => setIsModalVisible(false)}
  224. style={styles.modal}
  225. statusBarTranslucent={true}
  226. presentationStyle="overFullScreen"
  227. >
  228. <Formik
  229. initialValues={{
  230. comment: '',
  231. nm: region.nmRegion ? region.nmRegion.region_name : '-',
  232. dare: region.dareRegion ? region.dareRegion.name : '-',
  233. series: selectedSeries ? selectedSeries.id : null,
  234. name: marker?.name,
  235. link: marker?.url,
  236. lat: coordinates?.lat,
  237. lng: coordinates?.lng,
  238. item: -1
  239. }}
  240. validationSchema={SuggestionSchema}
  241. onSubmit={(values) => {
  242. const { comment, name, link, lat, lng, item } = values;
  243. if (!selectedSeries) return;
  244. const submitData: SubmitSuggestionTypes = {
  245. token,
  246. comment,
  247. name,
  248. link,
  249. lat,
  250. lng,
  251. item,
  252. nm: region.nmRegion.id,
  253. dare: region.dareRegion ? region.dareRegion.id : 0,
  254. series: selectedSeries.id
  255. };
  256. submitSuggestion(submitData, {
  257. onSuccess: () => {
  258. setSubmitedModalVisible(true);
  259. }
  260. });
  261. }}
  262. >
  263. {(props) => (
  264. <KeyboardAvoidingView
  265. behavior={'padding'}
  266. style={[styles.modalContent, { maxHeight: keyboardVisible ? undefined : '90%' }]}
  267. >
  268. <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
  269. <ScrollView
  270. contentContainerStyle={{ gap: 16 }}
  271. showsVerticalScrollIndicator={false}
  272. keyboardShouldPersistTaps="handled"
  273. >
  274. <Input
  275. placeholder="NM region"
  276. value={props.values.nm}
  277. editable={false}
  278. header="NM region"
  279. height={40}
  280. />
  281. <Input
  282. placeholder="DARE place"
  283. value={props.values.dare}
  284. editable={false}
  285. header="DARE place"
  286. height={40}
  287. />
  288. <SeriesSelector
  289. selectedSeries={selectedSeries}
  290. setSeriesVisible={setSeriesVisible}
  291. props={props}
  292. />
  293. <Input
  294. placeholder="Name"
  295. value={props.values.name}
  296. editable={false}
  297. header="Name"
  298. height={40}
  299. />
  300. <Input
  301. placeholder="URL"
  302. value={props.values.link}
  303. editable={false}
  304. header="Google maps link"
  305. height={40}
  306. />
  307. <Input
  308. multiline={true}
  309. header="Comment"
  310. value={props.values.comment}
  311. onChange={props.handleChange('comment')}
  312. formikError={props.touched.comment && props.errors.comment}
  313. />
  314. <View style={{ paddingBottom: 24, gap: 16 }}>
  315. <Button children="Send" onPress={props.handleSubmit} />
  316. <Button
  317. children="Close"
  318. onPress={() => setIsModalVisible(false)}
  319. variant={ButtonVariants.OPACITY}
  320. containerStyles={styles.closeBtn}
  321. textStyles={{ color: Colors.DARK_BLUE }}
  322. />
  323. </View>
  324. </ScrollView>
  325. </TouchableWithoutFeedback>
  326. </KeyboardAvoidingView>
  327. )}
  328. </Formik>
  329. <ReactModal
  330. isVisible={seriesVisible}
  331. onBackdropPress={() => setSeriesVisible(false)}
  332. style={styles.modal}
  333. statusBarTranslucent={true}
  334. presentationStyle="overFullScreen"
  335. >
  336. <View style={styles.modalWrapper}>
  337. <ScrollView
  338. style={{ paddingBottom: 16 }}
  339. showsVerticalScrollIndicator={false}
  340. nestedScrollEnabled={true}
  341. >
  342. {groupedSeries ? (
  343. <View style={{ minHeight: 100, paddingBottom: 16, paddingTop: 8 }}>
  344. <TouchableOpacity
  345. style={styles.seriesItem}
  346. onPress={() => {
  347. setSelectedSeries(null);
  348. setSeriesVisible(false);
  349. }}
  350. >
  351. <Text style={styles.seriesText}>Select series</Text>
  352. </TouchableOpacity>
  353. <FlashList
  354. viewabilityConfig={{
  355. waitForInteraction: true,
  356. itemVisiblePercentThreshold: 50,
  357. minimumViewTime: 1000
  358. }}
  359. estimatedItemSize={40}
  360. data={groupedSeries}
  361. renderItem={renderGroup}
  362. keyExtractor={(item) => item.title}
  363. nestedScrollEnabled={true}
  364. />
  365. </View>
  366. ) : null}
  367. </ScrollView>
  368. </View>
  369. </ReactModal>
  370. <WarningModal
  371. isVisible={submitedModalVisible}
  372. onClose={handleClose}
  373. type="success"
  374. message="Thank you for your suggestion!"
  375. title="Success!"
  376. />
  377. </ReactModal>
  378. </SafeAreaView>
  379. );
  380. };
  381. export default SuggestSeriesScreen;