index.tsx 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689
  1. import React, { useCallback, useEffect, useRef, useState } from 'react';
  2. import {
  3. View,
  4. Text,
  5. Image,
  6. TouchableOpacity,
  7. ScrollView,
  8. KeyboardAvoidingView,
  9. Platform,
  10. TouchableWithoutFeedback,
  11. Keyboard
  12. } from 'react-native';
  13. import { Formik } from 'formik';
  14. import * as yup from 'yup';
  15. import { useNavigation } from '@react-navigation/native';
  16. import { styles } from '../CreateEvent/styles';
  17. import { ButtonVariants } from 'src/types/components';
  18. import ImageView from 'better-react-native-image-viewing';
  19. import { StoreType, storage } from 'src/storage';
  20. import AddImgSvg from 'assets/icons/travels-screens/add-img.svg';
  21. import { Button, CheckBox, Header, Input, PageWrapper, WarningModal } from 'src/components';
  22. import { Colors } from 'src/theme';
  23. import { getFontSize } from 'src/utils';
  24. import { RichEditor, RichToolbar, actions } from 'react-native-pell-rich-editor';
  25. import CalendarSvg from '../../../../../assets/icons/calendar.svg';
  26. import RangeCalendar from 'src/components/Calendars/RangeCalendar';
  27. import {
  28. usePostAddSharedTripMutation,
  29. usePostCancelEventMutation,
  30. usePostGetPhotosForRegionMutation,
  31. usePostSetFullMutation,
  32. usePostUpdateEventMutation,
  33. usePostUpdateSharedTripMutation
  34. } from '@api/events';
  35. import { SheetManager } from 'react-native-actions-sheet';
  36. import PhotosForRegionModal from '../Components/PhotosForRegionModal/PhotosForRegionModal';
  37. import { API_HOST } from 'src/constants';
  38. import AddMapPinModal from '../Components/AddMapPinModal';
  39. import { NAVIGATION_PAGES } from 'src/types';
  40. import TrashSvg from 'assets/icons/travels-screens/trash-solid.svg';
  41. import { useGetRegionsWithFlagQuery } from '@api/regions';
  42. const EventSchema = yup.object({
  43. title: yup.string().required().min(3),
  44. start_date: yup.date().nullable().required(),
  45. end_date: yup.date().nullable().required(),
  46. tentative: yup.number().optional(),
  47. photo: yup.number().nullable().optional(),
  48. details: yup.string().required()
  49. });
  50. const CreateSharedTripScreen = ({ route }: { route: any }) => {
  51. const eventId = route.params?.eventId;
  52. const eventData = route.params?.event;
  53. const token = (storage.get('token', StoreType.STRING) as string) ?? null;
  54. const navigation = useNavigation();
  55. const scrollRef = useRef<ScrollView>(null);
  56. const richText = useRef<RichEditor | null>(null);
  57. const { mutateAsync: getPhotosForRegion } = usePostGetPhotosForRegionMutation();
  58. const { mutateAsync: addSharedTrip } = usePostAddSharedTripMutation();
  59. const { mutateAsync: updateEvent } = usePostUpdateSharedTripMutation();
  60. const { mutateAsync: cancelEvent } = usePostCancelEventMutation();
  61. const { mutateAsync: setFull } = usePostSetFullMutation();
  62. const { data: regionslist } = useGetRegionsWithFlagQuery(true);
  63. const [isSubmitting, setIsSubmitting] = useState(false);
  64. const [calendarVisible, setCalendarVisible] = useState<'start_date' | 'end_date' | null>(null);
  65. const [isViewerVisible, setIsViewerVisible] = useState(false);
  66. const [photos, setPhotos] = useState<any[]>([]);
  67. const [regions, setRegions] = useState<any[]>(route.params?.regionsToSave ?? []);
  68. const [regionsError, setRegionsError] = useState<string | null>(null);
  69. const [photosError, setPhotosError] = useState<string | null>(null);
  70. useEffect(() => {
  71. if (route.params?.regionsToSave) {
  72. setRegions(route.params.regionsToSave);
  73. }
  74. }, [route.params?.regionsToSave]);
  75. useEffect(() => {
  76. if (regions && regions.length > 0) {
  77. handleGetPhotosForAllRegions(regions);
  78. } else {
  79. setPhotos([]);
  80. }
  81. }, [regions]);
  82. const handleGetPhotosForAllRegions = useCallback(
  83. async (regionsArray: any[]) => {
  84. if (!regionsArray || regionsArray.length === 0) {
  85. setPhotos([]);
  86. return;
  87. }
  88. const allPhotos: any[] = [];
  89. try {
  90. for (const region of regionsArray) {
  91. await getPhotosForRegion(
  92. { region_id: region.id },
  93. {
  94. onSuccess: (res) => {
  95. if (res.photos && res.photos.length > 0) {
  96. allPhotos.push(...res.photos);
  97. }
  98. },
  99. onError: (error) => {
  100. console.log(`Error loading photos for region ${region.id}:`, error);
  101. }
  102. }
  103. );
  104. }
  105. setPhotos(allPhotos);
  106. } catch (error) {
  107. setPhotos([]);
  108. }
  109. },
  110. [getPhotosForRegion, token]
  111. );
  112. const initialData: any = {
  113. title: eventData?.title ?? '',
  114. start_date: eventData?.date_from ?? '',
  115. end_date: eventData?.date_to ?? '',
  116. tentative: eventData?.date_tentative ?? 0,
  117. photo: null,
  118. details: eventData?.details ?? ''
  119. };
  120. const [modalInfo, setModalInfo] = useState({
  121. visible: false,
  122. type: 'success',
  123. title: '',
  124. message: '',
  125. buttonTitle: 'OK',
  126. action: () => {}
  127. });
  128. useEffect(() => {
  129. if (eventData && eventData.regions && regionslist && regionslist.data) {
  130. const parsedRegions = JSON.parse(eventData.regions);
  131. let regionsData: any = [];
  132. parsedRegions.forEach((region: any) => {
  133. const regionWithFlag = regionslist.data?.find((r: any) => r.id === +region);
  134. regionsData.push({
  135. flag1: regionWithFlag?.flag ?? '',
  136. flag2: null,
  137. region_name: regionWithFlag?.name ?? '',
  138. id: regionWithFlag?.id ?? region.id
  139. });
  140. });
  141. setRegions(regionsData);
  142. }
  143. }, [eventData, regionslist]);
  144. const handleCancelTrip = () => {
  145. setModalInfo({
  146. visible: true,
  147. type: 'delete',
  148. title: 'Cancel trip',
  149. buttonTitle: 'Cancel',
  150. message: `Are you sure you want to cancel this trip?`,
  151. action: () => {
  152. cancelEvent(
  153. { token, event_id: eventId },
  154. {
  155. onSuccess: (res) => {
  156. navigation.navigate(NAVIGATION_PAGES.EVENTS as never);
  157. }
  158. }
  159. );
  160. }
  161. });
  162. };
  163. const handleDeleteRegion = (regionId: number) => {
  164. regions && setRegions(regions.filter((region) => region.id !== regionId));
  165. };
  166. return (
  167. <PageWrapper>
  168. <Header label="Add trip" />
  169. <KeyboardAvoidingView
  170. behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
  171. style={{ flex: 1 }}
  172. >
  173. <TouchableWithoutFeedback onPress={Keyboard.dismiss} accessible={false}>
  174. <ScrollView ref={scrollRef} showsVerticalScrollIndicator={false}>
  175. <Formik
  176. validationSchema={EventSchema}
  177. initialValues={initialData}
  178. onSubmit={async (values) => {
  179. if (regions.length === 0) {
  180. return;
  181. }
  182. if (photos.length > 0 && !values.photo) {
  183. return;
  184. }
  185. setIsSubmitting(true);
  186. const regionsToSave = regions.map((region) => region.id);
  187. const newTrip: any = {
  188. title: values.title,
  189. start_date: values.start_date,
  190. end_date: values.end_date,
  191. tentative: values.tentative,
  192. regions: regionsToSave,
  193. details: values.details
  194. };
  195. if (values.photo) {
  196. newTrip.photo = values.photo;
  197. }
  198. if (eventId) {
  199. await updateEvent(
  200. { token, trip_id: eventId, trip: JSON.stringify(newTrip) },
  201. {
  202. onSuccess: (res) => {
  203. setIsSubmitting(false);
  204. navigation.goBack();
  205. },
  206. onError: (err) => {
  207. setIsSubmitting(false);
  208. }
  209. }
  210. );
  211. } else {
  212. await addSharedTrip(
  213. { token, trip: JSON.stringify(newTrip) },
  214. {
  215. onSuccess: (res) => {
  216. setIsSubmitting(false);
  217. setModalInfo({
  218. visible: true,
  219. type: 'success',
  220. title: 'Success',
  221. buttonTitle: 'OK',
  222. message: `Thank you for adding this new trip. It will undergo review soon. You'll be notified via email once it is approved.`,
  223. action: () => {}
  224. });
  225. },
  226. onError: (err) => {
  227. setIsSubmitting(false);
  228. }
  229. }
  230. );
  231. }
  232. }}
  233. >
  234. {(props) => (
  235. <View style={{ gap: 12 }}>
  236. <Input
  237. header={'Trip name'}
  238. placeholder={'Add trip name'}
  239. inputMode={'text'}
  240. onChange={props.handleChange('title')}
  241. value={props.values.title}
  242. onBlur={props.handleBlur('title')}
  243. formikError={props.touched.title && (props.errors.title as string)}
  244. />
  245. <View style={{ flexDirection: 'row', gap: 8 }}>
  246. <View style={{ flex: 1 }}>
  247. <Input
  248. header={'From'}
  249. placeholder={'Add start date'}
  250. inputMode={'none'}
  251. onChange={props.handleChange('start_date')}
  252. value={props.values.start_date}
  253. onBlur={props.handleBlur('start_date')}
  254. isFocused={() => setCalendarVisible('start_date')}
  255. formikError={
  256. props.touched.start_date && (props.errors.start_date as string)
  257. }
  258. icon={<CalendarSvg fill={Colors.LIGHT_GRAY} width={20} height={20} />}
  259. />
  260. </View>
  261. <View style={{ flex: 1 }}>
  262. <Input
  263. header={'To'}
  264. placeholder={'Add end date'}
  265. inputMode={'none'}
  266. onChange={props.handleChange('end_date')}
  267. value={props.values.end_date}
  268. onBlur={props.handleBlur('end_date')}
  269. isFocused={() => setCalendarVisible('end_date')}
  270. formikError={props.touched.end_date && (props.errors.end_date as string)}
  271. icon={<CalendarSvg fill={Colors.LIGHT_GRAY} width={20} height={20} />}
  272. />
  273. </View>
  274. </View>
  275. <TouchableOpacity
  276. onPress={() => {
  277. props.setFieldValue('tentative', props.values.tentative === 1 ? 0 : 1);
  278. }}
  279. style={styles.optionBtn}
  280. hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
  281. >
  282. <Text style={styles.optionText}>Tentative dates</Text>
  283. <CheckBox
  284. onChange={() => {
  285. props.setFieldValue('tentative', props.values.tentative === 1 ? 0 : 1);
  286. }}
  287. value={props.values.tentative === 1}
  288. color={Colors.DARK_BLUE}
  289. />
  290. </TouchableOpacity>
  291. <View>
  292. <Text
  293. style={{
  294. color: Colors.DARK_BLUE,
  295. fontSize: getFontSize(14),
  296. fontFamily: 'redhat-700',
  297. marginBottom: 8
  298. }}
  299. >
  300. NomadMania regions
  301. </Text>
  302. <TouchableOpacity
  303. style={styles.addRegionBtn}
  304. onPress={() =>
  305. navigation.navigate(
  306. ...([
  307. NAVIGATION_PAGES.ADD_REGIONS,
  308. { regionsParams: regions, editId: eventId, isSharedTrip: true }
  309. ] as never)
  310. )
  311. }
  312. >
  313. <Text style={styles.addRegionBtntext}>Add Region</Text>
  314. </TouchableOpacity>
  315. {regions && regions.length ? (
  316. <View style={styles.regionsContainer}>
  317. {regions.map((region) => {
  318. const [name, ...rest] = region.region_name?.split(/ – | - /);
  319. const subname = rest?.join(' - ');
  320. return (
  321. <View key={region.id} style={styles.regionItem}>
  322. <View style={styles.regionHeader}>
  323. <Image
  324. source={{ uri: `${API_HOST}/img/flags_new/${region.flag1}` }}
  325. style={styles.flagStyle}
  326. />
  327. {region.flag2 && (
  328. <Image
  329. source={{
  330. uri: `${API_HOST}/img/flags_new/${region.flag2}`
  331. }}
  332. style={[styles.flagStyle, { marginLeft: -17 }]}
  333. />
  334. )}
  335. <View style={styles.nameContainer}>
  336. <Text style={styles.regionName}>{name}</Text>
  337. <Text style={styles.regionSubname}>{subname}</Text>
  338. </View>
  339. </View>
  340. <TouchableOpacity
  341. style={styles.trashBtn}
  342. onPress={() => handleDeleteRegion(region.id)}
  343. >
  344. <TrashSvg fill={Colors.WHITE} />
  345. </TouchableOpacity>
  346. </View>
  347. );
  348. })}
  349. </View>
  350. ) : regionsError ? (
  351. <Text
  352. style={{
  353. color: Colors.RED,
  354. fontSize: getFontSize(12),
  355. fontFamily: 'redhat-600',
  356. marginTop: 5
  357. }}
  358. >
  359. {regionsError}
  360. </Text>
  361. ) : (
  362. <Text style={styles.noRegiosText}>No regions at the moment</Text>
  363. )}
  364. </View>
  365. {regions && regions.length > 0 && photos.length > 0 ? (
  366. <Input
  367. header={'Photo'}
  368. placeholder={props.values.photo ? 'Photo selected' : 'Choose a photo'}
  369. inputMode={'none'}
  370. onBlur={props.handleBlur('photo')}
  371. isFocused={() =>
  372. SheetManager.show('photos-for-region-modal', {
  373. payload: {
  374. photos,
  375. selectPhoto: (photo: any) => {
  376. props.setFieldValue('photo', photo);
  377. setPhotosError(null);
  378. }
  379. } as any
  380. })
  381. }
  382. formikError={props.touched.photo && (props.errors.photo as string)}
  383. icon={<AddImgSvg fill={Colors.LIGHT_GRAY} width={20} height={20} />}
  384. />
  385. ) : null}
  386. {props.values.photo ? (
  387. <View
  388. style={{
  389. display: 'flex',
  390. justifyContent: 'center',
  391. alignItems: 'center',
  392. gap: 8
  393. }}
  394. >
  395. <TouchableOpacity
  396. style={{ width: '100%' }}
  397. onPress={async () => setIsViewerVisible(true)}
  398. >
  399. <Image
  400. source={{ uri: `${API_HOST}/webapi/photos/${props.values.photo}/small` }}
  401. style={{ width: '100%', height: 200, borderRadius: 4 }}
  402. />
  403. </TouchableOpacity>
  404. </View>
  405. ) : null}
  406. {photosError ? (
  407. <Text
  408. style={{
  409. color: Colors.RED,
  410. fontSize: getFontSize(12),
  411. fontFamily: 'redhat-600',
  412. marginTop: -5
  413. }}
  414. >
  415. {photosError}
  416. </Text>
  417. ) : null}
  418. <View>
  419. <Text
  420. style={{
  421. color: Colors.DARK_BLUE,
  422. fontSize: getFontSize(14),
  423. fontFamily: 'redhat-700',
  424. marginBottom: 8
  425. }}
  426. >
  427. Details
  428. </Text>
  429. <RichToolbar
  430. editor={richText}
  431. selectedButtonStyle={{ backgroundColor: Colors.LIGHT_GRAY }}
  432. style={[
  433. {
  434. borderTopLeftRadius: 4,
  435. borderTopRightRadius: 4,
  436. backgroundColor: Colors.FILL_LIGHT
  437. },
  438. props.touched.details && props.errors.details
  439. ? {
  440. borderTopColor: Colors.RED,
  441. borderLeftColor: Colors.RED,
  442. borderRightColor: Colors.RED,
  443. borderWidth: 1,
  444. borderBottomWidth: 0
  445. }
  446. : {}
  447. ]}
  448. actions={[
  449. actions.keyboard,
  450. actions.setBold,
  451. actions.setItalic,
  452. actions.setUnderline,
  453. actions.alignLeft,
  454. actions.alignCenter,
  455. actions.alignRight,
  456. actions.alignFull,
  457. actions.insertLink,
  458. actions.insertBulletsList,
  459. actions.insertOrderedList,
  460. actions.setStrikethrough,
  461. actions.indent,
  462. actions.outdent,
  463. actions.undo,
  464. actions.redo,
  465. actions.blockquote,
  466. actions.insertLine
  467. ]}
  468. />
  469. <RichEditor
  470. ref={richText}
  471. initialHeight={100}
  472. onFocus={() => {
  473. scrollRef.current?.scrollTo({
  474. y: 650,
  475. animated: true
  476. });
  477. }}
  478. placeholder="Add trip details"
  479. initialContentHTML={props.values.details}
  480. onChange={props.handleChange('details')}
  481. editorStyle={{
  482. contentCSSText: 'font-size: 14px; padding: 12px; color: #C8C8C8;',
  483. backgroundColor: Colors.FILL_LIGHT,
  484. color: Colors.DARK_BLUE
  485. }}
  486. style={[
  487. {
  488. minHeight: 100,
  489. borderBottomLeftRadius: 4,
  490. borderBottomRightRadius: 4
  491. },
  492. props.touched.details && props.errors.details
  493. ? {
  494. borderBottomColor: Colors.RED,
  495. borderLeftColor: Colors.RED,
  496. borderRightColor: Colors.RED,
  497. borderWidth: 1,
  498. borderTopWidth: 0
  499. }
  500. : {}
  501. ]}
  502. onBlur={() => props.handleBlur('details')}
  503. />
  504. {props.touched.details && props.errors.details ? (
  505. <Text
  506. style={{
  507. color: Colors.RED,
  508. fontSize: getFontSize(12),
  509. fontFamily: 'redhat-600',
  510. marginTop: 5
  511. }}
  512. >
  513. {props.errors.details as string}
  514. </Text>
  515. ) : null}
  516. </View>
  517. {eventId ? (
  518. <View style={{ marginTop: 15, marginBottom: 15, gap: 8 }}>
  519. <Button
  520. onPress={() => {
  521. if (regions.length === 0) {
  522. setRegionsError('please add at least one region');
  523. } else {
  524. setRegionsError(null);
  525. }
  526. if (photos.length > 0 && !props.values.photo) {
  527. setPhotosError('please select a photo');
  528. } else {
  529. setPhotosError(null);
  530. }
  531. props.handleSubmit();
  532. }}
  533. disabled={isSubmitting}
  534. >
  535. Update trip
  536. </Button>
  537. <Button
  538. variant={ButtonVariants.OPACITY}
  539. containerStyles={{
  540. backgroundColor: Colors.WHITE,
  541. borderColor: Colors.BORDER_LIGHT
  542. }}
  543. textStyles={{ color: Colors.DARK_BLUE }}
  544. onPress={async () => {
  545. await setFull({ token, id: eventId, full: 1 });
  546. navigation.goBack();
  547. }}
  548. >
  549. Set Full
  550. </Button>
  551. <Button
  552. variant={ButtonVariants.OPACITY}
  553. containerStyles={{
  554. backgroundColor: Colors.RED,
  555. borderColor: Colors.RED
  556. }}
  557. textStyles={{ color: Colors.WHITE }}
  558. onPress={handleCancelTrip}
  559. >
  560. Cancel trip
  561. </Button>
  562. </View>
  563. ) : (
  564. <View style={{ marginTop: 15, marginBottom: 15, gap: 8 }}>
  565. <Button
  566. onPress={() => {
  567. if (regions.length === 0) {
  568. setRegionsError('please add at least one region');
  569. } else {
  570. setRegionsError(null);
  571. }
  572. if (photos.length > 0 && !props.values.photo) {
  573. setPhotosError('please select a photo');
  574. } else {
  575. setPhotosError(null);
  576. }
  577. props.handleSubmit();
  578. }}
  579. disabled={isSubmitting}
  580. >
  581. Save
  582. </Button>
  583. <Button
  584. variant={ButtonVariants.OPACITY}
  585. containerStyles={{
  586. backgroundColor: Colors.WHITE,
  587. borderColor: Colors.BORDER_LIGHT
  588. }}
  589. textStyles={{ color: Colors.DARK_BLUE }}
  590. onPress={() => navigation.goBack()}
  591. >
  592. Cancel
  593. </Button>
  594. </View>
  595. )}
  596. <RangeCalendar
  597. isModalVisible={calendarVisible ? true : false}
  598. closeModal={(startDate?: string | null, endDate?: string | null) => {
  599. startDate && props.handleChange(calendarVisible)(startDate.toString());
  600. setCalendarVisible(null);
  601. }}
  602. allowRangeSelection={false}
  603. selectedDate={props.values[calendarVisible ?? 'start_date']}
  604. disablePastDates={true}
  605. />
  606. <ImageView
  607. images={[
  608. {
  609. uri: `${API_HOST}/webapi/photos/${props.values.photo}/full`
  610. }
  611. ]}
  612. keyExtractor={(imageSrc, index) => index.toString()}
  613. imageIndex={0}
  614. visible={isViewerVisible}
  615. onRequestClose={() => setIsViewerVisible(false)}
  616. swipeToCloseEnabled={false}
  617. backgroundColor={Colors.DARK_BLUE}
  618. />
  619. </View>
  620. )}
  621. </Formik>
  622. </ScrollView>
  623. </TouchableWithoutFeedback>
  624. </KeyboardAvoidingView>
  625. <PhotosForRegionModal />
  626. <AddMapPinModal />
  627. <WarningModal
  628. type={modalInfo.type}
  629. isVisible={modalInfo.visible}
  630. buttonTitle={modalInfo.buttonTitle}
  631. message={modalInfo.message}
  632. action={modalInfo.action}
  633. onClose={() => {
  634. if (modalInfo.type === 'success') {
  635. navigation.goBack();
  636. setModalInfo({ ...modalInfo, visible: false });
  637. } else {
  638. setModalInfo({ ...modalInfo, visible: false });
  639. }
  640. }}
  641. title={modalInfo.title}
  642. />
  643. </PageWrapper>
  644. );
  645. };
  646. export default CreateSharedTripScreen;