index.tsx 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350
  1. import React, { FC, useCallback, useEffect, useRef, useState } from 'react';
  2. import {
  3. View,
  4. Text,
  5. Image,
  6. TouchableOpacity,
  7. Linking,
  8. Dimensions,
  9. FlatList,
  10. Platform,
  11. ActivityIndicator
  12. } from 'react-native';
  13. import { styles } from './styles';
  14. import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
  15. import { Colors } from 'src/theme';
  16. import FileViewer from 'react-native-file-viewer';
  17. import * as FileSystem from 'expo-file-system';
  18. import * as DocumentPicker from 'react-native-document-picker';
  19. import * as ImagePicker from 'expo-image-picker';
  20. import { ScrollView } from 'react-native-gesture-handler';
  21. import { NAVIGATION_PAGES } from 'src/types';
  22. import { API_HOST, APP_VERSION } from 'src/constants';
  23. import { StoreType, storage } from 'src/storage';
  24. import { MaterialCommunityIcons } from '@expo/vector-icons';
  25. import * as Progress from 'react-native-progress';
  26. import ChevronLeft from 'assets/icons/chevron-left.svg';
  27. import MapSvg from 'assets/icons/travels-screens/map-location.svg';
  28. import AddImgSvg from 'assets/icons/travels-screens/add-img.svg';
  29. import ShareIcon from 'assets/icons/share.svg';
  30. import GigtIcon from 'assets/icons/events/gift.svg';
  31. import CalendarCrossedIcon from 'assets/icons/events/calendar-crossed.svg';
  32. import CalendarCheckIcon from 'assets/icons/events/calendar-check.svg';
  33. import CalendarIcon from 'assets/icons/events/calendar-solid.svg';
  34. import EarthIcon from 'assets/icons/travels-section/earth.svg';
  35. import NomadsIcon from 'assets/icons/bottom-navigation/travellers.svg';
  36. import LocationIcon from 'assets/icons/bottom-navigation/map.svg';
  37. import FileIcon from 'assets/icons/events/file-solid.svg';
  38. import ImageIcon from 'assets/icons/events/image.svg';
  39. import { getFontSize } from 'src/utils';
  40. import {
  41. EventAttachments,
  42. EventData,
  43. EventPhotos,
  44. useGetEventQuery,
  45. usePostDeleteFileMutation,
  46. usePostEventAddFileMutation,
  47. usePostJoinEventMutation,
  48. usePostUnjoinEventMutation,
  49. usePostUploadPhotoMutation,
  50. usePostUploadTempFileMutation
  51. } from '@api/events';
  52. import { AvatarWithInitials, Input, Loading, WarningModal } from 'src/components';
  53. import moment from 'moment';
  54. import { renderSpotsText } from '../EventsScreen/utils';
  55. import { useWindowDimensions } from 'react-native';
  56. import RenderHtml, { HTMLElementModel, TNode } from 'react-native-render-html';
  57. import { PhotoItem } from './PhotoItem';
  58. import Share from 'react-native-share';
  59. import { CACHED_ATTACHMENTS_DIR } from 'src/constants/constants';
  60. import { Dropdown } from 'react-native-searchable-dropdown-kj';
  61. import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
  62. import Tooltip from 'react-native-walkthrough-tooltip';
  63. import WebView from 'react-native-webview';
  64. type TempFile = {
  65. filetype: string;
  66. name: string;
  67. temp_name: string;
  68. isSending: boolean;
  69. type: 1 | 2 | 3;
  70. description: string;
  71. };
  72. const fileWidth = Dimensions.get('window').width / 5;
  73. const EventScreen = ({ route }: { route: any }) => {
  74. const eventUrl = route.params?.url;
  75. const token = (storage.get('token', StoreType.STRING) as string) ?? null;
  76. const currentUserId = (storage.get('uid', StoreType.NUMBER) as number) ?? 0;
  77. const navigation = useNavigation();
  78. const { width: windowWidth } = useWindowDimensions();
  79. const contentWidth = windowWidth * 0.9;
  80. const scrollViewRef = useRef<ScrollView>(null);
  81. const { data, refetch } = useGetEventQuery(token, eventUrl, true);
  82. const { mutateAsync: joinEvent } = usePostJoinEventMutation();
  83. const { mutateAsync: unjoinEvent } = usePostUnjoinEventMutation();
  84. const { mutateAsync: uploadTempFile } = usePostUploadTempFileMutation();
  85. const { mutateAsync: saveFile } = usePostEventAddFileMutation();
  86. const { mutateAsync: deleteFile } = usePostDeleteFileMutation();
  87. const { mutateAsync: uploadPhoto } = usePostUploadPhotoMutation();
  88. const [isExpanded, setIsExpanded] = useState(false);
  89. const [tooltipUser, setTooltipUser] = useState<number | null>(null);
  90. const [event, setEvent] = useState<EventData | null>(null);
  91. const [registrationInfo, setRegistrationInfo] = useState<{ color: string; name: string } | null>(
  92. null
  93. );
  94. const [filteredParticipants, setFilteredParticipants] = useState<EventData['participants_data']>(
  95. []
  96. );
  97. const [maxVisibleParticipants, setMaxVisibleParticipants] = useState(0);
  98. const [maxVisibleParticipantsWithGap, setMaxVisibleParticipantsWithGap] = useState(0);
  99. const [joined, setJoined] = useState<0 | 1>(0);
  100. const [uploadProgress, setUploadProgress] = useState<{ [key: string]: number }>({});
  101. const [myTempFiles, setMyTempFiles] = useState<TempFile[]>([]);
  102. const [myFiles, setMyFiles] = useState<EventAttachments[]>([]);
  103. const [photos, setPhotos] = useState<(EventPhotos & { isSending?: boolean })[]>([]);
  104. const [isUploading, setIsUploading] = useState(false);
  105. const [modalInfo, setModalInfo] = useState({
  106. visible: false,
  107. type: 'success',
  108. title: '',
  109. message: '',
  110. buttonTitle: 'OK',
  111. action: () => {}
  112. });
  113. useEffect(() => {
  114. if (data && data.data) {
  115. setEvent(data.data);
  116. setJoined(data.data.joined);
  117. setMyFiles(data.data.files ?? []);
  118. setPhotos(data.data.photos);
  119. const partisipantsWidth = contentWidth / 2;
  120. setMaxVisibleParticipants(Math.floor(partisipantsWidth / 22));
  121. setMaxVisibleParticipantsWithGap(Math.floor(partisipantsWidth / 32));
  122. setFilteredParticipants(data.data.participants_data);
  123. setRegistrationInfo(() => {
  124. if (data.data.full) {
  125. return {
  126. color: Colors.LIGHT_GRAY,
  127. name: 'FULL'
  128. };
  129. } else if (data.data.settings.type === 2) {
  130. return {
  131. color: Colors.ORANGE,
  132. name: 'TOUR'
  133. };
  134. } else if (data.data.settings.type === 3) {
  135. return {
  136. color: Colors.DARK_BLUE,
  137. name: 'CONF'
  138. };
  139. }
  140. return null;
  141. });
  142. }
  143. }, [data]);
  144. useFocusEffect(
  145. useCallback(() => {
  146. refetch();
  147. }, [navigation])
  148. );
  149. const handlePreviewDocument = useCallback(async (url: string, fileName: string) => {
  150. try {
  151. const dirExist = await FileSystem.getInfoAsync(CACHED_ATTACHMENTS_DIR);
  152. if (!dirExist.exists) {
  153. await FileSystem.makeDirectoryAsync(CACHED_ATTACHMENTS_DIR, { intermediates: true });
  154. }
  155. const fileUri = `${CACHED_ATTACHMENTS_DIR}${fileName}`;
  156. const fileExists = await FileSystem.getInfoAsync(fileUri);
  157. if (fileExists.exists && fileExists.size > 1024) {
  158. await FileViewer.open(fileUri, {
  159. showOpenWithDialog: true,
  160. showAppsSuggestions: true
  161. });
  162. return;
  163. }
  164. const downloadResumable = FileSystem.createDownloadResumable(
  165. url,
  166. fileUri,
  167. {},
  168. (downloadProgress) => {
  169. const progress =
  170. downloadProgress.totalBytesWritten / downloadProgress.totalBytesExpectedToWrite;
  171. setUploadProgress((prev) => ({ ...prev, [fileName]: progress * 100 }));
  172. }
  173. );
  174. const { uri: localUri } = await FileSystem.downloadAsync(url, fileUri, {
  175. headers: { Nmtoken: token, 'App-Version': APP_VERSION, Platform: Platform.OS }
  176. });
  177. await FileViewer.open(localUri, {
  178. showOpenWithDialog: true,
  179. showAppsSuggestions: true
  180. });
  181. } catch (error) {
  182. console.error('Error previewing document:', error);
  183. } finally {
  184. setUploadProgress((prev) => {
  185. const newProgress = { ...prev };
  186. delete newProgress[fileName];
  187. return newProgress;
  188. });
  189. }
  190. }, []);
  191. const handleUploadFile = useCallback(async () => {
  192. try {
  193. const response = await DocumentPicker.pick({
  194. type: [DocumentPicker.types.allFiles],
  195. allowMultiSelection: true
  196. });
  197. setIsUploading(true);
  198. for (const res of response) {
  199. let file: any = {
  200. uri: res.uri,
  201. name: res.name,
  202. type: res.type
  203. };
  204. if ((file.name && !file.name.includes('.')) || !file.type) {
  205. file = {
  206. ...file,
  207. type: file.type || 'application/octet-stream'
  208. };
  209. }
  210. await uploadTempFile(
  211. {
  212. token,
  213. file,
  214. onUploadProgress: (progressEvent) => {
  215. // if (progressEvent.lengthComputable) {
  216. // const progress = Math.round(
  217. // (progressEvent.loaded / (progressEvent.total ?? 100)) * 100
  218. // );
  219. // setUploadProgress((prev) => ({ ...prev, [file!.uri]: progress }));
  220. // }
  221. }
  222. },
  223. {
  224. onSuccess: (result) => {
  225. setMyTempFiles((prev) => [
  226. { ...result, type: 1, description: '', isSending: false },
  227. ...prev
  228. ]);
  229. setIsUploading(false);
  230. },
  231. onError: (error) => {
  232. console.error('Upload error:', error);
  233. }
  234. }
  235. );
  236. }
  237. } catch {
  238. setIsUploading(false);
  239. } finally {
  240. setIsUploading(false);
  241. }
  242. }, [token]);
  243. const handleUploadPhoto = useCallback(async () => {
  244. if (!event) return;
  245. try {
  246. const perm = await ImagePicker.requestMediaLibraryPermissionsAsync();
  247. if (!perm.granted) {
  248. console.warn('Permission for gallery not granted');
  249. return;
  250. }
  251. const result = await ImagePicker.launchImageLibraryAsync({
  252. mediaTypes: ImagePicker.MediaTypeOptions.Images,
  253. allowsMultipleSelection: true,
  254. quality: 1,
  255. selectionLimit: 4
  256. });
  257. if (!result.canceled && result.assets) {
  258. const files = result.assets.map((asset) => ({
  259. uri: asset.uri,
  260. type: asset.mimeType ?? 'image',
  261. name: asset.uri ? (asset.uri.split('/').pop() as string) : 'image'
  262. }));
  263. for (const file of files) {
  264. const staticPhoto: any = {
  265. id: new Date().getTime(),
  266. filetype: file.type,
  267. uid: +currentUserId,
  268. name: '',
  269. avatar: null,
  270. isSending: true,
  271. preview: 1,
  272. data: 1
  273. };
  274. setPhotos((prev) => [staticPhoto, ...prev]);
  275. await uploadPhoto(
  276. {
  277. token,
  278. event_id: event.id,
  279. file,
  280. onUploadProgress: (progressEvent) => {
  281. // if (progressEvent.lengthComputable) {
  282. // const progress = Math.round(
  283. // (progressEvent.loaded / (progressEvent.total ?? 100)) * 100
  284. // );
  285. // setUploadProgress((prev) => ({ ...prev, [file!.uri]: progress }));
  286. // }
  287. }
  288. },
  289. {
  290. onSuccess: (result) => {
  291. refetch();
  292. },
  293. onError: () => {
  294. refetch();
  295. }
  296. }
  297. );
  298. }
  299. }
  300. } catch {}
  301. }, [token, event]);
  302. if (!event) return <Loading />;
  303. const handleShare = async () => {
  304. if (!event) return;
  305. try {
  306. // TO DO
  307. const uri = `${API_HOST}/event/${eventUrl}`;
  308. if (uri) {
  309. await Share.open({ url: uri });
  310. }
  311. } catch (error) {
  312. console.error('Error sharing the event url:', error);
  313. }
  314. };
  315. const handleJoinEvent = async () => {
  316. if (event.settings.type !== 1) {
  317. setModalInfo({
  318. visible: true,
  319. type: 'success',
  320. title: 'Success',
  321. buttonTitle: 'OK',
  322. message: `Thank you for joing, we’ll get back to you soon.`,
  323. action: () => {}
  324. });
  325. }
  326. await joinEvent(
  327. { token, id: event.id },
  328. {
  329. onSuccess: () => {
  330. setJoined(1);
  331. refetch();
  332. }
  333. }
  334. );
  335. };
  336. const handleUnjoinEvent = async () => {
  337. await unjoinEvent(
  338. { token, id: event.id },
  339. {
  340. onSuccess: () => {
  341. setJoined(0);
  342. refetch();
  343. }
  344. }
  345. );
  346. };
  347. const handleDeleteFile = async (file: EventAttachments) => {
  348. setModalInfo({
  349. visible: true,
  350. type: 'delete',
  351. title: 'Delete file',
  352. buttonTitle: 'Delete',
  353. message: `Are you sure you want to delete this file?`,
  354. action: async () => {
  355. await deleteFile(
  356. {
  357. token,
  358. id: file.id,
  359. event_id: event.id
  360. },
  361. {
  362. onSuccess: () => {
  363. setMyFiles(myFiles.filter((f) => f.id !== file.id));
  364. }
  365. }
  366. );
  367. }
  368. });
  369. };
  370. const renderItem = ({ item, index }: { item: EventAttachments; index: number }) => {
  371. const totalItems = event.attachments.length;
  372. if (!isExpanded && index === 7 && totalItems > 8) {
  373. return (
  374. <TouchableOpacity
  375. style={{
  376. width: fileWidth,
  377. alignItems: 'center',
  378. gap: 4
  379. }}
  380. onPress={() => {
  381. setIsExpanded(true);
  382. }}
  383. >
  384. <View
  385. style={{
  386. backgroundColor: Colors.FILL_LIGHT,
  387. borderRadius: 8,
  388. alignItems: 'center',
  389. justifyContent: 'center',
  390. height: fileWidth,
  391. width: fileWidth
  392. }}
  393. >
  394. <MaterialCommunityIcons name="dots-horizontal" size={36} color={Colors.DARK_BLUE} />
  395. </View>
  396. </TouchableOpacity>
  397. );
  398. }
  399. return (
  400. <TouchableOpacity
  401. style={{
  402. width: fileWidth,
  403. alignItems: 'center',
  404. gap: 4
  405. }}
  406. onPress={() =>
  407. handlePreviewDocument(
  408. API_HOST + '/webapi/events/get-attachment/' + event.id + '/' + item.id,
  409. event.id + '-' + item.filename
  410. )
  411. }
  412. >
  413. <View
  414. style={{
  415. backgroundColor: Colors.FILL_LIGHT,
  416. borderRadius: 8,
  417. alignItems: 'center',
  418. justifyContent: 'center',
  419. height: fileWidth,
  420. width: fileWidth
  421. }}
  422. >
  423. <MaterialCommunityIcons
  424. name={item.filetype.startsWith('image') ? 'image' : 'file'}
  425. size={36}
  426. color={Colors.DARK_BLUE}
  427. />
  428. </View>
  429. <Text
  430. style={{ fontSize: 12, fontWeight: '600', color: Colors.DARK_BLUE }}
  431. numberOfLines={2}
  432. >
  433. {item.filename}
  434. </Text>
  435. </TouchableOpacity>
  436. );
  437. };
  438. const renderItemFile = ({ item, index }: { item: EventAttachments; index: number }) => {
  439. return (
  440. <TouchableOpacity
  441. style={{
  442. flexDirection: 'row',
  443. alignItems: 'center',
  444. gap: 8,
  445. backgroundColor: Colors.FILL_LIGHT,
  446. flex: 1,
  447. paddingHorizontal: 8,
  448. paddingVertical: 12,
  449. borderRadius: 8
  450. }}
  451. onPress={() => {
  452. handlePreviewDocument(
  453. `${API_HOST}/webapi/events/get-file/${event.id}/${item.id}/?token=${token}`,
  454. item.filename
  455. );
  456. }}
  457. >
  458. <View style={{ gap: 8, flex: 3.5 }}>
  459. <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8, flex: 1 }}>
  460. <FileIcon fill={Colors.DARK_BLUE} height={18} />
  461. <Text style={{ color: Colors.DARK_BLUE, fontSize: 13, fontWeight: '600' }}>
  462. {item.filename}
  463. </Text>
  464. </View>
  465. <Text style={{ color: Colors.TEXT_GRAY, fontSize: 12, fontWeight: '500' }}>
  466. {item.type === 1 ? 'passport' : item.type === 2 ? 'disclaimer' : 'other'}
  467. </Text>
  468. {item.description ? (
  469. <Text style={{ color: Colors.DARK_BLUE, fontSize: 13, fontWeight: '500' }}>
  470. {item.description}
  471. </Text>
  472. ) : null}
  473. </View>
  474. <View style={{ flex: 1 }}>
  475. <TouchableOpacity
  476. style={{
  477. flexDirection: 'row',
  478. alignItems: 'center',
  479. justifyContent: 'center',
  480. gap: 8,
  481. backgroundColor: Colors.RED,
  482. paddingVertical: 8,
  483. paddingHorizontal: 4,
  484. borderRadius: 20
  485. }}
  486. onPress={() => handleDeleteFile(item)}
  487. >
  488. <Text
  489. style={{
  490. color: Colors.WHITE,
  491. fontSize: getFontSize(13),
  492. fontWeight: '700'
  493. }}
  494. >
  495. Delete
  496. </Text>
  497. </TouchableOpacity>
  498. </View>
  499. </TouchableOpacity>
  500. );
  501. };
  502. const formatEventDate = (event: EventData) => {
  503. if (event.settings.date_from && event.settings.date_to) {
  504. return `${moment(event.settings.date_from, 'YYYY-MM-DD').format('DD MMMM YYYY')} - ${moment(event.settings.date_to, 'YYYY-MM-DD').format('DD MMMM YYYY')}`;
  505. } else {
  506. let date = moment(event.settings.date, 'YYYY-MM-DD').format('DD MMMM YYYY');
  507. if (event.time) {
  508. date += `, ${event.time}`;
  509. }
  510. return date;
  511. }
  512. };
  513. const handleSaveFile = async (file: TempFile) => {
  514. setMyTempFiles(() =>
  515. myTempFiles.map((f) => (f.temp_name === file.temp_name ? { ...f, isSending: true } : f))
  516. );
  517. await saveFile(
  518. {
  519. token,
  520. event_id: event.id,
  521. type: file.type,
  522. description: file.description,
  523. filetype: file.filetype,
  524. filename: file.name,
  525. temp_filename: file.temp_name
  526. },
  527. {
  528. onSuccess: () => {
  529. setMyTempFiles(myTempFiles.filter((f) => f.temp_name !== file.temp_name));
  530. refetch();
  531. },
  532. onError: () => {
  533. setMyTempFiles(() =>
  534. myTempFiles.map((f) =>
  535. f.temp_name === file.temp_name ? { ...f, isSending: false } : f
  536. )
  537. );
  538. }
  539. }
  540. );
  541. };
  542. const staticImgUrl =
  543. event.type === 2
  544. ? '/static/img/events/trip.webp'
  545. : event.type === 3
  546. ? '/static/img/events/conference.webp'
  547. : '/static/img/events/meeting.webp';
  548. const photoUrl = event.photo_available
  549. ? API_HOST + '/webapi/events/get-main-photo/' + event.id
  550. : API_HOST + staticImgUrl;
  551. return (
  552. <View style={styles.container}>
  553. <TouchableOpacity
  554. onPress={() => {
  555. navigation.goBack();
  556. }}
  557. style={styles.backButton}
  558. >
  559. <View style={styles.chevronWrapper}>
  560. <ChevronLeft fill={Colors.WHITE} />
  561. </View>
  562. </TouchableOpacity>
  563. <KeyboardAwareScrollView showsVerticalScrollIndicator={false}>
  564. <ScrollView
  565. ref={scrollViewRef}
  566. contentContainerStyle={{ minHeight: '100%' }}
  567. nestedScrollEnabled={true}
  568. showsVerticalScrollIndicator={false}
  569. removeClippedSubviews={false}
  570. >
  571. <Image source={{ uri: photoUrl }} style={{ width: '100%', height: 220 }} />
  572. {/* <TouchableOpacity
  573. onPress={() => {
  574. // navigation.dispatch(
  575. // CommonActions.reset({
  576. // index: 1,
  577. // routes: [
  578. // {
  579. // name: NAVIGATION_PAGES.IN_APP_MAP_TAB,
  580. // state: {
  581. // routes: [
  582. // {
  583. // name: NAVIGATION_PAGES.MAP_TAB,
  584. // params: { id: regionId, type: type === 'nm' ? 'regions' : 'places' }
  585. // }
  586. // ]
  587. // }
  588. // }
  589. // ]
  590. // })
  591. // )
  592. }}
  593. style={styles.goToMapBtn}
  594. >
  595. <View style={styles.chevronWrapper}>
  596. <MapSvg fill={Colors.WHITE} />
  597. </View>
  598. </TouchableOpacity> */}
  599. {registrationInfo && (
  600. <View
  601. style={{
  602. position: 'absolute',
  603. width: 71,
  604. height: 31,
  605. top: 170,
  606. left: 0,
  607. justifyContent: 'center',
  608. alignItems: 'center',
  609. zIndex: 2,
  610. backgroundColor: registrationInfo.color,
  611. borderTopRightRadius: 4,
  612. borderBottomRightRadius: 4,
  613. paddingRight: 10,
  614. paddingLeft: 16
  615. }}
  616. >
  617. <Text
  618. style={{
  619. textTransform: 'uppercase',
  620. fontSize: getFontSize(16),
  621. fontWeight: '700',
  622. color: Colors.WHITE
  623. }}
  624. >
  625. {registrationInfo.name}
  626. </Text>
  627. </View>
  628. )}
  629. <View style={styles.wrapper}>
  630. <View style={styles.nameContainer}>
  631. <Text style={styles.title}>{event.settings.name}</Text>
  632. <TouchableOpacity
  633. onPress={handleShare}
  634. style={{
  635. alignItems: 'center',
  636. justifyContent: 'center',
  637. paddingLeft: 8,
  638. marginLeft: 4
  639. }}
  640. >
  641. <ShareIcon
  642. width={20}
  643. height={20}
  644. fill={Colors.DARK_BLUE}
  645. style={{ alignSelf: 'center' }}
  646. />
  647. </TouchableOpacity>
  648. </View>
  649. <View style={styles.divider} />
  650. <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
  651. <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4, flex: 1 }}>
  652. <CalendarIcon fill={Colors.DARK_BLUE} height={20} width={20} />
  653. <Text
  654. style={{
  655. fontSize: getFontSize(12),
  656. fontWeight: '600',
  657. color: Colors.DARK_BLUE,
  658. flex: 1
  659. }}
  660. >
  661. {formatEventDate(event)}
  662. </Text>
  663. </View>
  664. <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4, flex: 1 }}>
  665. <EarthIcon fill={Colors.DARK_BLUE} height={20} width={20} />
  666. <Text
  667. style={{
  668. fontSize: getFontSize(12),
  669. fontWeight: '600',
  670. color: Colors.DARK_BLUE,
  671. flex: 1
  672. }}
  673. >
  674. {event.settings.address1}
  675. </Text>
  676. </View>
  677. </View>
  678. <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
  679. {event.settings.address2 && (
  680. <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4, flex: 1 }}>
  681. <LocationIcon fill={Colors.DARK_BLUE} height={20} width={20} />
  682. <Text
  683. style={{
  684. fontSize: getFontSize(12),
  685. fontWeight: '600',
  686. color: Colors.DARK_BLUE,
  687. flex: 1
  688. }}
  689. >
  690. {event.settings.address2}
  691. </Text>
  692. </View>
  693. )}
  694. {event.settings.registrations_info !== 1 && (
  695. <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4, flex: 1 }}>
  696. <NomadsIcon fill={Colors.DARK_BLUE} height={20} width={20} />
  697. <Text
  698. style={{
  699. fontSize: getFontSize(12),
  700. fontWeight: '600',
  701. color: Colors.DARK_BLUE,
  702. flex: 1
  703. }}
  704. >
  705. {renderSpotsText(event)}
  706. </Text>
  707. </View>
  708. )}
  709. </View>
  710. <View style={styles.stats}>
  711. {event.settings.host_data ? (
  712. <View style={{ gap: 8, flex: 1 }}>
  713. <Text style={[styles.travelSeriesTitle, { fontSize: 12 }]}>Host</Text>
  714. <TouchableOpacity
  715. style={[styles.statItem, { justifyContent: 'flex-start' }]}
  716. onPress={() =>
  717. navigation.navigate(
  718. ...([
  719. NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW,
  720. {
  721. userId: event.settings.host_profile
  722. }
  723. ] as never)
  724. )
  725. }
  726. disabled={!event.settings.host_profile}
  727. >
  728. <View style={styles.userImageContainer}>
  729. {event.settings.host_data.avatar ? (
  730. <Image
  731. source={{
  732. uri: API_HOST + '/img/avatars/' + event.settings.host_data.avatar
  733. }}
  734. style={[styles.userImage, { marginLeft: 0 }]}
  735. />
  736. ) : null}
  737. <View style={{ justifyContent: 'space-between' }}>
  738. <Text
  739. style={{
  740. fontFamily: 'montserrat-700',
  741. fontSize: 12,
  742. color: Colors.DARK_BLUE
  743. }}
  744. >
  745. {event.settings.host_data.first_name} {event.settings.host_data.last_name}
  746. </Text>
  747. <Text style={{ fontWeight: '600', fontSize: 12, color: Colors.DARK_BLUE }}>
  748. NM:{' '}
  749. <Text style={{ fontFamily: 'montserrat-700' }}>
  750. {event.settings.host_data.nm}
  751. </Text>{' '}
  752. / UN:{' '}
  753. <Text style={{ fontFamily: 'montserrat-700' }}>
  754. {event.settings.host_data.un}
  755. </Text>
  756. </Text>
  757. </View>
  758. </View>
  759. </TouchableOpacity>
  760. </View>
  761. ) : (
  762. <View style={[styles.statItem, { justifyContent: 'flex-start' }]} />
  763. )}
  764. {filteredParticipants.length > 0 ? (
  765. <View style={{ gap: 8, flex: 1 }}>
  766. <Text style={[styles.travelSeriesTitle, { fontSize: 12 }]}>Participants</Text>
  767. <View style={[styles.statItem, { justifyContent: 'flex-start' }]}>
  768. <View style={styles.userImageContainer}>
  769. {(filteredParticipants.length > maxVisibleParticipants
  770. ? filteredParticipants.slice(0, maxVisibleParticipants - 1)
  771. : filteredParticipants
  772. ).map((user, index) => (
  773. <Tooltip
  774. isVisible={tooltipUser === index}
  775. content={
  776. <TouchableOpacity
  777. onPress={() => {
  778. setTooltipUser(null);
  779. navigation.navigate(
  780. ...([
  781. NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW,
  782. { userId: user.uid }
  783. ] as never)
  784. );
  785. }}
  786. >
  787. <Text>{user.name}</Text>
  788. </TouchableOpacity>
  789. }
  790. contentStyle={{ backgroundColor: Colors.FILL_LIGHT }}
  791. placement="top"
  792. onClose={() => setTooltipUser(null)}
  793. key={index}
  794. backgroundColor="transparent"
  795. >
  796. <TouchableOpacity onPress={() => setTooltipUser(index)}>
  797. {user.avatar ? (
  798. <Image
  799. key={index}
  800. source={{ uri: API_HOST + user.avatar }}
  801. style={[
  802. styles.userImage,
  803. (filteredParticipants.length > maxVisibleParticipantsWithGap ||
  804. filteredParticipants.length < (event.participants ?? 0)) &&
  805. index !== 0
  806. ? { marginLeft: -10 }
  807. : {}
  808. ]}
  809. />
  810. ) : (
  811. <AvatarWithInitials
  812. text={
  813. user.name?.split(' ')[0][0] + (user.name?.split(' ')[1][0] ?? '')
  814. }
  815. flag={API_HOST + user?.flag}
  816. size={28}
  817. fontSize={12}
  818. borderColor={Colors.DARK_LIGHT}
  819. borderWidth={1}
  820. />
  821. )}
  822. </TouchableOpacity>
  823. </Tooltip>
  824. ))}
  825. {maxVisibleParticipants < filteredParticipants.length ||
  826. filteredParticipants.length < (event.participants ?? 0) ? (
  827. <View style={styles.userCountContainer}>
  828. <Text style={styles.userCount}>{event.participants}</Text>
  829. </View>
  830. ) : null}
  831. </View>
  832. </View>
  833. </View>
  834. ) : (
  835. <View style={[styles.statItem, { justifyContent: 'flex-end' }]} />
  836. )}
  837. </View>
  838. {joined ? (
  839. <TouchableOpacity
  840. style={{
  841. flexDirection: 'row',
  842. alignItems: 'center',
  843. justifyContent: 'center',
  844. paddingVertical: 8,
  845. paddingHorizontal: 12,
  846. borderRadius: 20,
  847. backgroundColor: Colors.WHITE,
  848. gap: 6,
  849. borderWidth: 1,
  850. borderColor: Colors.DARK_BLUE
  851. }}
  852. onPress={handleUnjoinEvent}
  853. >
  854. <CalendarCrossedIcon fill={Colors.DARK_BLUE} width={16} height={16} />
  855. <Text
  856. style={{
  857. color: Colors.DARK_BLUE,
  858. fontSize: getFontSize(14),
  859. fontFamily: 'montserrat-700'
  860. }}
  861. >
  862. Cancel
  863. </Text>
  864. </TouchableOpacity>
  865. ) : !event.full ? (
  866. <TouchableOpacity
  867. style={{
  868. flexDirection: 'row',
  869. alignItems: 'center',
  870. justifyContent: 'center',
  871. paddingVertical: 8,
  872. paddingHorizontal: 12,
  873. borderRadius: 20,
  874. backgroundColor: Colors.ORANGE,
  875. gap: 6,
  876. borderWidth: 1,
  877. borderColor: Colors.ORANGE
  878. }}
  879. onPress={handleJoinEvent}
  880. >
  881. {event.settings.free ? (
  882. <>
  883. <GigtIcon fill={Colors.WHITE} width={16} height={16} />
  884. <Text
  885. style={{
  886. color: Colors.WHITE,
  887. fontSize: getFontSize(14),
  888. fontFamily: 'montserrat-700',
  889. textTransform: 'uppercase'
  890. }}
  891. >
  892. Join - free
  893. </Text>
  894. </>
  895. ) : (
  896. <>
  897. <CalendarCheckIcon fill={Colors.WHITE} width={16} height={16} />
  898. <Text
  899. style={{
  900. color: Colors.WHITE,
  901. fontSize: getFontSize(14),
  902. fontFamily: 'montserrat-700',
  903. textTransform: 'uppercase'
  904. }}
  905. >
  906. Interested
  907. </Text>
  908. </>
  909. )}
  910. </TouchableOpacity>
  911. ) : null}
  912. <View style={[styles.divider]} />
  913. {event.settings.details && event.settings.details.length ? (
  914. <View style={{ gap: 8 }}>
  915. <Text style={styles.travelSeriesTitle}>Details</Text>
  916. <WebDisplay html={event.settings.details} />
  917. </View>
  918. ) : null}
  919. {event.attachments.length > 0 ? (
  920. <View style={{ gap: 16 }}>
  921. <Text style={styles.travelSeriesTitle}>Attachments</Text>
  922. <FlatList
  923. data={isExpanded ? event.attachments : event.attachments.slice(0, 8)}
  924. renderItem={renderItem}
  925. keyExtractor={(item) => item.id.toString()}
  926. numColumns={4}
  927. columnWrapperStyle={{
  928. justifyContent: 'flex-start',
  929. gap: 12
  930. }}
  931. contentContainerStyle={{
  932. gap: 8,
  933. alignSelf: 'center'
  934. }}
  935. showsVerticalScrollIndicator={false}
  936. scrollEnabled={false}
  937. />
  938. </View>
  939. ) : null}
  940. {(photos && photos.length) ||
  941. (event.joined && event.settings.participants_can_add_photos) ? (
  942. <View style={{ gap: 16 }}>
  943. <View
  944. style={{
  945. flexDirection: 'row',
  946. justifyContent: 'space-between',
  947. alignItems: 'center'
  948. }}
  949. >
  950. <Text style={styles.travelSeriesTitle}>Photos</Text>
  951. {event.settings.participants_can_add_photos && event.joined ? (
  952. <TouchableOpacity
  953. style={{
  954. flexDirection: 'row',
  955. backgroundColor: Colors.ORANGE,
  956. gap: 6,
  957. alignItems: 'center',
  958. justifyContent: 'center',
  959. paddingVertical: 7,
  960. paddingHorizontal: 12,
  961. borderRadius: 20
  962. }}
  963. onPress={handleUploadPhoto}
  964. >
  965. <Text
  966. style={{
  967. fontSize: getFontSize(13),
  968. fontWeight: '700',
  969. color: Colors.WHITE
  970. }}
  971. >
  972. Add
  973. </Text>
  974. <ImageIcon fill={Colors.WHITE} width={18} height={18} />
  975. </TouchableOpacity>
  976. ) : null}
  977. </View>
  978. {photos && photos.length > 0 ? (
  979. <PhotoItem photos={photos} eventId={event.id} photosLeft={event.photos_left} />
  980. ) : null}
  981. </View>
  982. ) : null}
  983. {(event.files && event.files.length) ||
  984. (event.joined && event.settings.participants_can_add_files) ? (
  985. <View style={{ gap: 16, paddingBottom: 16 }}>
  986. <View
  987. style={{
  988. flexDirection: 'row',
  989. justifyContent: 'space-between',
  990. alignItems: 'center'
  991. }}
  992. >
  993. <Text style={styles.travelSeriesTitle}>My files</Text>
  994. {event.settings.participants_can_add_files && event.joined ? (
  995. <TouchableOpacity
  996. style={{
  997. flexDirection: 'row',
  998. backgroundColor: Colors.ORANGE,
  999. gap: 6,
  1000. alignItems: 'center',
  1001. justifyContent: 'center',
  1002. paddingVertical: 7,
  1003. paddingHorizontal: 12,
  1004. borderRadius: 20
  1005. }}
  1006. onPress={handleUploadFile}
  1007. >
  1008. <Text
  1009. style={{
  1010. fontSize: getFontSize(13),
  1011. fontWeight: '700',
  1012. color: Colors.WHITE
  1013. }}
  1014. >
  1015. Add
  1016. </Text>
  1017. <FileIcon fill={Colors.WHITE} height={18} />
  1018. </TouchableOpacity>
  1019. ) : null}
  1020. </View>
  1021. {isUploading && (
  1022. <View
  1023. style={{
  1024. alignItems: 'center',
  1025. justifyContent: 'center'
  1026. }}
  1027. >
  1028. <Progress.CircleSnail
  1029. borderWidth={0}
  1030. color={Colors.DARK_BLUE}
  1031. unfilledColor="rgba(0, 0, 0, 0.1)"
  1032. />
  1033. </View>
  1034. )}
  1035. {myTempFiles && myTempFiles.length
  1036. ? myTempFiles.map((file) => {
  1037. return (
  1038. <View
  1039. key={file.temp_name}
  1040. style={{
  1041. flexDirection: 'row',
  1042. alignItems: 'center',
  1043. gap: 8,
  1044. backgroundColor: Colors.FILL_LIGHT,
  1045. flex: 1,
  1046. paddingHorizontal: 8,
  1047. paddingVertical: 12,
  1048. borderRadius: 8
  1049. }}
  1050. >
  1051. <View style={{ gap: 8, flex: 3 }}>
  1052. <View
  1053. style={{
  1054. flexDirection: 'row',
  1055. alignItems: 'center',
  1056. gap: 8,
  1057. flex: 1
  1058. }}
  1059. >
  1060. <FileIcon fill={Colors.DARK_BLUE} height={18} />
  1061. <Text
  1062. style={{ color: Colors.DARK_BLUE, fontSize: 13, fontWeight: '500' }}
  1063. >
  1064. {file.name}
  1065. </Text>
  1066. </View>
  1067. <Input
  1068. height={36}
  1069. backgroundColor={Colors.WHITE}
  1070. placeholder="Add comment here"
  1071. multiline={true}
  1072. onChange={(text) => {
  1073. setMyTempFiles(() =>
  1074. myTempFiles.map((f) =>
  1075. f.temp_name === file.temp_name ? { ...f, description: text } : f
  1076. )
  1077. );
  1078. }}
  1079. />
  1080. <Dropdown
  1081. style={{
  1082. height: 36,
  1083. backgroundColor: Colors.WHITE,
  1084. borderRadius: 4,
  1085. paddingHorizontal: 8
  1086. }}
  1087. placeholderStyle={{
  1088. fontSize: 14,
  1089. color: Colors.DARK_BLUE,
  1090. fontWeight: '500'
  1091. }}
  1092. selectedTextStyle={{
  1093. fontSize: 14,
  1094. color: Colors.DARK_BLUE,
  1095. fontWeight: '500'
  1096. }}
  1097. data={[
  1098. { label: 'passport', value: 1 },
  1099. { label: 'disclaimer', value: 2 },
  1100. { label: 'other', value: 3 }
  1101. ]}
  1102. labelField="label"
  1103. valueField="value"
  1104. value={file.type}
  1105. placeholder="First visit"
  1106. onChange={(item: { value: 1 | 2 | 3 }) => {
  1107. setMyTempFiles(() =>
  1108. myTempFiles.map((f) =>
  1109. f.temp_name === file.temp_name ? { ...f, type: item.value } : f
  1110. )
  1111. );
  1112. }}
  1113. containerStyle={{ borderRadius: 4 }}
  1114. renderItem={(item) => (
  1115. <View style={{ paddingVertical: 12, paddingHorizontal: 16 }}>
  1116. <Text
  1117. style={{
  1118. fontSize: 14,
  1119. color: Colors.DARK_BLUE,
  1120. fontWeight: '500'
  1121. }}
  1122. >
  1123. {item.label}
  1124. </Text>
  1125. </View>
  1126. )}
  1127. />
  1128. </View>
  1129. <View style={{ flex: 1 }}>
  1130. <TouchableOpacity
  1131. style={{
  1132. flexDirection: 'row',
  1133. alignItems: 'center',
  1134. justifyContent: 'center',
  1135. gap: 8,
  1136. backgroundColor: Colors.DARK_BLUE,
  1137. paddingVertical: 8,
  1138. paddingHorizontal: 4,
  1139. borderRadius: 20
  1140. }}
  1141. onPress={() => handleSaveFile(file)}
  1142. disabled={file.isSending}
  1143. >
  1144. {file.isSending ? (
  1145. <View>
  1146. <ActivityIndicator
  1147. size={16}
  1148. color={Colors.WHITE}
  1149. style={{ transform: 'scale(0.9)' }}
  1150. />
  1151. </View>
  1152. ) : (
  1153. <Text
  1154. style={{
  1155. color: Colors.WHITE,
  1156. fontSize: getFontSize(13),
  1157. fontWeight: '700'
  1158. }}
  1159. >
  1160. Save
  1161. </Text>
  1162. )}
  1163. </TouchableOpacity>
  1164. </View>
  1165. </View>
  1166. );
  1167. })
  1168. : null}
  1169. <FlatList
  1170. data={myFiles}
  1171. renderItem={renderItemFile}
  1172. keyExtractor={(item) => item.id.toString()}
  1173. contentContainerStyle={{ gap: 8 }}
  1174. showsVerticalScrollIndicator={false}
  1175. scrollEnabled={false}
  1176. />
  1177. </View>
  1178. ) : null}
  1179. </View>
  1180. </ScrollView>
  1181. </KeyboardAwareScrollView>
  1182. <WarningModal
  1183. type={modalInfo.type}
  1184. isVisible={modalInfo.visible}
  1185. buttonTitle={modalInfo.buttonTitle}
  1186. message={modalInfo.message}
  1187. action={modalInfo.action}
  1188. onClose={() => setModalInfo({ ...modalInfo, visible: false })}
  1189. title={modalInfo.title}
  1190. />
  1191. </View>
  1192. );
  1193. };
  1194. const iframeModel = HTMLElementModel.fromCustomModel({
  1195. tagName: 'iframe',
  1196. contentModel: 'block' as any
  1197. });
  1198. const WebDisplay = React.memo(function WebDisplay({ html }: { html: string }) {
  1199. const { width: windowWidth } = useWindowDimensions();
  1200. const contentWidth = windowWidth * 0.9;
  1201. const token = storage.get('token', StoreType.STRING) as string;
  1202. const processedHtml = React.useMemo(() => {
  1203. let updatedHtml = html;
  1204. const hrefRegex = /href="((?!http)[^"]+)"/g;
  1205. const imgSrcRegex = /src="((?:\.{0,2}\/)*img\/[^"]*)"/g;
  1206. const normalizePath = (path: string): string => {
  1207. const segments = path.split('/').filter(Boolean);
  1208. const resolved: string[] = [];
  1209. for (const segment of segments) {
  1210. if (segment === '..') resolved.pop();
  1211. else if (segment !== '.') resolved.push(segment);
  1212. }
  1213. return '/' + resolved.join('/');
  1214. };
  1215. updatedHtml = updatedHtml
  1216. .replace(hrefRegex, (match, rawPath) => {
  1217. const normalizedPath = normalizePath(rawPath);
  1218. const fullUrl = `${API_HOST}${normalizedPath}`;
  1219. if (normalizedPath.includes('shop')) {
  1220. const separator = fullUrl.includes('?') ? '&' : '?';
  1221. return `href="${fullUrl}${separator}token=${encodeURIComponent(token)}"`;
  1222. }
  1223. return `href="${fullUrl}"`;
  1224. })
  1225. .replace(imgSrcRegex, (_match, rawSrc) => {
  1226. const normalizedImg = normalizePath(rawSrc);
  1227. return `src="${API_HOST}${normalizedImg}"`;
  1228. });
  1229. return updatedHtml;
  1230. }, [html, token]);
  1231. const renderers = {
  1232. iframe: ({ tnode }: { tnode: TNode }) => {
  1233. const src = tnode.attributes.src || '';
  1234. const width = contentWidth;
  1235. const height = Number(tnode.attributes.height) || 250;
  1236. return (
  1237. <WebView
  1238. source={{ uri: src }}
  1239. style={{ width, height }}
  1240. javaScriptEnabled
  1241. domStorageEnabled
  1242. startInLoadingState
  1243. scalesPageToFit
  1244. />
  1245. );
  1246. }
  1247. };
  1248. const customHTMLElementModels = {
  1249. iframe: iframeModel
  1250. };
  1251. return (
  1252. <RenderHtml
  1253. contentWidth={contentWidth}
  1254. source={{ html: processedHtml }}
  1255. customHTMLElementModels={customHTMLElementModels}
  1256. renderers={renderers}
  1257. />
  1258. );
  1259. });
  1260. export default EventScreen;