index.tsx 77 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148
  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. Animated
  13. } from 'react-native';
  14. import { styles } from './styles';
  15. import { CommonActions, useFocusEffect, useNavigation } from '@react-navigation/native';
  16. import { Colors } from 'src/theme';
  17. import FileViewer from 'react-native-file-viewer';
  18. import * as FileSystem from 'expo-file-system';
  19. import * as DocumentPicker from 'react-native-document-picker';
  20. import * as ImagePicker from 'expo-image-picker';
  21. import { ScrollView } from 'react-native-gesture-handler';
  22. import { NAVIGATION_PAGES } from 'src/types';
  23. import { API_HOST, APP_VERSION } from 'src/constants';
  24. import { StoreType, storage } from 'src/storage';
  25. import { MaterialCommunityIcons } from '@expo/vector-icons';
  26. import * as Progress from 'react-native-progress';
  27. import ImageView from 'better-react-native-image-viewing';
  28. import ChevronLeft from 'assets/icons/chevron-left.svg';
  29. import MapSvg from 'assets/icons/travels-screens/map-location.svg';
  30. import AddImgSvg from 'assets/icons/travels-screens/add-img.svg';
  31. import ShareIcon from 'assets/icons/share.svg';
  32. import GigtIcon from 'assets/icons/events/gift.svg';
  33. import CalendarCrossedIcon from 'assets/icons/events/calendar-crossed.svg';
  34. import CalendarCheckIcon from 'assets/icons/events/calendar-check.svg';
  35. import CalendarIcon from 'assets/icons/events/calendar-solid.svg';
  36. import EarthIcon from 'assets/icons/travels-section/earth.svg';
  37. import NomadsIcon from 'assets/icons/bottom-navigation/travellers.svg';
  38. import LocationIcon from 'assets/icons/bottom-navigation/map.svg';
  39. import FileIcon from 'assets/icons/events/file-solid.svg';
  40. import ImageIcon from 'assets/icons/events/image.svg';
  41. import { getFontSize } from 'src/utils';
  42. import {
  43. EventAttachments,
  44. EventData,
  45. EventPhotos,
  46. useGetEventForEditingMutation,
  47. useGetEventQuery,
  48. useGetPhotosForRegionQuery,
  49. usePostDeleteFileMutation,
  50. usePostEventAddFileMutation,
  51. usePostGetPhotosForRegionMutation,
  52. usePostJoinEventMutation,
  53. usePostUnjoinEventMutation,
  54. usePostUploadPhotoMutation,
  55. usePostUploadTempFileMutation
  56. } from '@api/events';
  57. import { AvatarWithInitials, Input, Loading, WarningModal } from 'src/components';
  58. import moment from 'moment';
  59. import { renderSpotsText } from '../EventsScreen/utils';
  60. import { useWindowDimensions } from 'react-native';
  61. import RenderHtml, { HTMLElementModel, TNode } from 'react-native-render-html';
  62. import { PhotoItem } from './PhotoItem';
  63. import Share from 'react-native-share';
  64. import { CACHED_ATTACHMENTS_DIR } from 'src/constants/constants';
  65. import { Dropdown } from 'react-native-searchable-dropdown-kj';
  66. import { KeyboardAwareScrollView } from 'react-native-keyboard-aware-scroll-view';
  67. import Tooltip from 'react-native-walkthrough-tooltip';
  68. import WebView from 'react-native-webview';
  69. import { PhotosData } from '../../MapScreen/RegionViewScreen/types';
  70. import ImageCarousel from '../../MapScreen/RegionViewScreen/ImageCarousel';
  71. import EditSvg from 'assets/icons/events/edit.svg';
  72. import ChatIcon from 'assets/icons/bottom-navigation/messages.svg';
  73. import InfoIcon from 'assets/icons/info-solid.svg';
  74. type TempFile = {
  75. filetype: string;
  76. name: string;
  77. temp_name: string;
  78. isSending: boolean;
  79. type: 1 | 2 | 3;
  80. description: string;
  81. };
  82. const fileWidth = Dimensions.get('window').width / 5;
  83. const EventScreen = ({ route }: { route: any }) => {
  84. const eventUrl = route.params?.url;
  85. const token = (storage.get('token', StoreType.STRING) as string) ?? null;
  86. const currentUserId = (storage.get('uid', StoreType.STRING) as string) ?? 0;
  87. const navigation = useNavigation();
  88. const { width: windowWidth } = useWindowDimensions();
  89. const contentWidth = windowWidth * 0.9;
  90. const scrollViewRef = useRef<ScrollView>(null);
  91. const keyboardAwareScrollViewRef = useRef<KeyboardAwareScrollView>(null);
  92. const { data, refetch } = useGetEventQuery(token, eventUrl, true);
  93. const { mutateAsync: joinEvent } = usePostJoinEventMutation();
  94. const { mutateAsync: unjoinEvent } = usePostUnjoinEventMutation();
  95. const { mutateAsync: uploadTempFile } = usePostUploadTempFileMutation();
  96. const { mutateAsync: saveFile } = usePostEventAddFileMutation();
  97. const { mutateAsync: deleteFile } = usePostDeleteFileMutation();
  98. const { mutateAsync: uploadPhoto } = usePostUploadPhotoMutation();
  99. const { mutateAsync: getForEditing } = useGetEventForEditingMutation();
  100. const [isExpanded, setIsExpanded] = useState(false);
  101. const [tooltipUser, setTooltipUser] = useState<number | null>(null);
  102. const [tooltipInterested, setTooltipInterested] = useState<number | null>(null);
  103. const [event, setEvent] = useState<EventData | null>(null);
  104. const [registrationInfo, setRegistrationInfo] = useState<{ color: string; name: string } | null>(
  105. null
  106. );
  107. const [filteredParticipants, setFilteredParticipants] = useState<EventData['participants_data']>(
  108. []
  109. );
  110. const [filteredInterested, setFilteredInterested] = useState<EventData['participants_data']>([]);
  111. const [maxVisibleParticipants, setMaxVisibleParticipants] = useState(0);
  112. const [maxVisibleParticipantsWithGap, setMaxVisibleParticipantsWithGap] = useState(0);
  113. const [joined, setJoined] = useState<0 | 1>(0);
  114. const [uploadProgress, setUploadProgress] = useState<{ [key: string]: number }>({});
  115. const [myTempFiles, setMyTempFiles] = useState<TempFile[]>([]);
  116. const [myFiles, setMyFiles] = useState<EventAttachments[]>([]);
  117. const [photos, setPhotos] = useState<(EventPhotos & { isSending?: boolean })[]>([]);
  118. const [isUploading, setIsUploading] = useState(false);
  119. const [photosForRegion, setPhotosForRegion] = useState<{ uriSmall: string; uri: string }[]>([]);
  120. const [activeIndex, setActiveIndex] = useState(0);
  121. const [isImageModalVisible, setIsImageModalVisible] = useState(false);
  122. const [currentImageIndex, setCurrentImageIndex] = useState(0);
  123. const [nmId, setNmId] = useState<number | null>(null);
  124. const [tooltipVisible, setTooltipVisible] = useState(false);
  125. const [isWarningModalVisible, setIsWarningModalVisible] = useState<boolean>(false);
  126. const { data: photosData } = useGetPhotosForRegionQuery(nmId ?? 0, nmId !== null);
  127. const { mutateAsync: getPhotosForRegion } = usePostGetPhotosForRegionMutation();
  128. const [regions, setRegions] = useState<any[]>([]);
  129. const [modalInfo, setModalInfo] = useState({
  130. visible: false,
  131. type: 'success',
  132. title: '',
  133. message: '',
  134. buttonTitle: 'OK',
  135. action: () => {}
  136. });
  137. const [showScrollToTop, setShowScrollToTop] = useState(false);
  138. const scrollToTopOpacity = useRef(new Animated.Value(0)).current;
  139. const [toolTipVisible, setToolTipVisible] = useState<boolean>(false);
  140. useEffect(() => {
  141. if (regions && regions.length > 0 && event && event.type === 4) {
  142. handleGetPhotosForAllRegions(regions);
  143. } else {
  144. setPhotosForRegion([]);
  145. }
  146. }, [regions, event]);
  147. const handleGetPhotosForAllRegions = useCallback(
  148. async (regionsArray: any[]) => {
  149. if (!regionsArray || regionsArray.length === 0) {
  150. setPhotosForRegion([]);
  151. return;
  152. }
  153. const allPhotos: any[] = [];
  154. try {
  155. for (const region of regionsArray) {
  156. await getPhotosForRegion(
  157. { region_id: region },
  158. {
  159. onSuccess: (res) => {
  160. if (res.photos && res.photos.length > 0) {
  161. allPhotos.push(...res.photos);
  162. }
  163. },
  164. onError: (error) => {
  165. console.log(`Error loading photos for region ${region}:`, error);
  166. }
  167. }
  168. );
  169. }
  170. setPhotosForRegion(
  171. allPhotos.map((item) => ({
  172. uriSmall: `${API_HOST}/ajax/pic/${item}/small`,
  173. uri: `${API_HOST}/ajax/pic/${item}/full`
  174. }))
  175. );
  176. } catch (error) {
  177. setPhotosForRegion([]);
  178. }
  179. },
  180. [getPhotosForRegion, token]
  181. );
  182. useEffect(() => {
  183. photosData &&
  184. setPhotosForRegion(
  185. photosData?.photos?.map((item) => ({
  186. uriSmall: `${API_HOST}/ajax/pic/${item}/small`,
  187. uri: `${API_HOST}/ajax/pic/${item}/full`
  188. })) ?? []
  189. );
  190. }, [photosData]);
  191. useEffect(() => {
  192. if (data && data.data) {
  193. setEvent(data.data);
  194. setJoined(data.data.joined);
  195. setNmId(data.data.settings.nm_region ?? null);
  196. if (data.data.settings.nm_regions && data.data.type === 4) {
  197. const ids = JSON.parse(data.data.settings.nm_regions);
  198. setRegions(ids);
  199. }
  200. setMyFiles(data.data.files ?? []);
  201. setPhotos(data.data.photos);
  202. const partisipantsWidth = contentWidth / 2;
  203. setMaxVisibleParticipants(Math.floor(partisipantsWidth / 24));
  204. setMaxVisibleParticipantsWithGap(Math.floor(partisipantsWidth / 32));
  205. setFilteredParticipants(
  206. data.data.type === 4 ? data.data.participants_approved_data : data.data.participants_data
  207. );
  208. setFilteredInterested(data.data.participants_data);
  209. setRegistrationInfo(() => {
  210. if (data.data.full) {
  211. return {
  212. color: Colors.LIGHT_GRAY,
  213. name: 'FULL'
  214. };
  215. } else if (data.data.closed) {
  216. return {
  217. color: Colors.LIGHT_GRAY,
  218. name: 'CLOSED'
  219. };
  220. } else if (data.data.settings.type === 2) {
  221. return {
  222. color: Colors.ORANGE,
  223. name: 'TOUR'
  224. };
  225. } else if (data.data.settings.type === 3) {
  226. return {
  227. color: Colors.DARK_BLUE,
  228. name: 'CONF'
  229. };
  230. }
  231. return null;
  232. });
  233. }
  234. }, [data]);
  235. useFocusEffect(
  236. useCallback(() => {
  237. refetch();
  238. }, [navigation])
  239. );
  240. const handleScroll = (event: any) => {
  241. const currentScrollY = event.nativeEvent.contentOffset.y;
  242. const shouldShow = currentScrollY > 350;
  243. if (shouldShow !== showScrollToTop) {
  244. setShowScrollToTop(shouldShow);
  245. Animated.timing(scrollToTopOpacity, {
  246. toValue: shouldShow ? 0.8 : 0,
  247. duration: 300,
  248. useNativeDriver: true
  249. }).start();
  250. }
  251. };
  252. const scrollToTop = () => {
  253. keyboardAwareScrollViewRef.current?.scrollToPosition(0, 0, true);
  254. };
  255. const openModal = (index: number) => {
  256. setCurrentImageIndex(index);
  257. setIsImageModalVisible(true);
  258. };
  259. const handlePreviewDocument = useCallback(async (url: string, fileName: string) => {
  260. try {
  261. const dirExist = await FileSystem.getInfoAsync(CACHED_ATTACHMENTS_DIR);
  262. if (!dirExist.exists) {
  263. await FileSystem.makeDirectoryAsync(CACHED_ATTACHMENTS_DIR, { intermediates: true });
  264. }
  265. const fileUri = `${CACHED_ATTACHMENTS_DIR}${fileName}`;
  266. const fileExists = await FileSystem.getInfoAsync(fileUri);
  267. if (fileExists.exists && fileExists.size > 1024) {
  268. await FileViewer.open(fileUri, {
  269. showOpenWithDialog: true,
  270. showAppsSuggestions: true
  271. });
  272. return;
  273. }
  274. const downloadResumable = FileSystem.createDownloadResumable(
  275. url,
  276. fileUri,
  277. {},
  278. (downloadProgress) => {
  279. const progress =
  280. downloadProgress.totalBytesWritten / downloadProgress.totalBytesExpectedToWrite;
  281. setUploadProgress((prev) => ({ ...prev, [fileName]: progress * 100 }));
  282. }
  283. );
  284. const { uri: localUri } = await FileSystem.downloadAsync(url, fileUri, {
  285. headers: { Nmtoken: token, 'App-Version': APP_VERSION, Platform: Platform.OS }
  286. });
  287. await FileViewer.open(localUri, {
  288. showOpenWithDialog: true,
  289. showAppsSuggestions: true
  290. });
  291. } catch (error) {
  292. console.error('Error previewing document:', error);
  293. } finally {
  294. setUploadProgress((prev) => {
  295. const newProgress = { ...prev };
  296. delete newProgress[fileName];
  297. return newProgress;
  298. });
  299. }
  300. }, []);
  301. const handleUploadFile = useCallback(async () => {
  302. try {
  303. const response = await DocumentPicker.pick({
  304. type: [DocumentPicker.types.allFiles],
  305. allowMultiSelection: true
  306. });
  307. setIsUploading(true);
  308. for (const res of response) {
  309. let file: any = {
  310. uri: res.uri,
  311. name: res.name,
  312. type: res.type
  313. };
  314. if ((file.name && !file.name.includes('.')) || !file.type) {
  315. file = {
  316. ...file,
  317. type: file.type || 'application/octet-stream'
  318. };
  319. }
  320. await uploadTempFile(
  321. {
  322. token,
  323. file,
  324. onUploadProgress: (progressEvent) => {
  325. // if (progressEvent.lengthComputable) {
  326. // const progress = Math.round(
  327. // (progressEvent.loaded / (progressEvent.total ?? 100)) * 100
  328. // );
  329. // setUploadProgress((prev) => ({ ...prev, [file!.uri]: progress }));
  330. // }
  331. }
  332. },
  333. {
  334. onSuccess: (result) => {
  335. setMyTempFiles((prev) => [
  336. { ...result, type: 1, description: '', isSending: false },
  337. ...prev
  338. ]);
  339. setIsUploading(false);
  340. },
  341. onError: (error) => {
  342. console.error('Upload error:', error);
  343. }
  344. }
  345. );
  346. }
  347. } catch {
  348. setIsUploading(false);
  349. } finally {
  350. setIsUploading(false);
  351. }
  352. }, [token]);
  353. const handleUploadPhoto = useCallback(async () => {
  354. if (!event) return;
  355. try {
  356. const perm = await ImagePicker.requestMediaLibraryPermissionsAsync();
  357. if (!perm.granted) {
  358. console.warn('Permission for gallery not granted');
  359. return;
  360. }
  361. const result = await ImagePicker.launchImageLibraryAsync({
  362. mediaTypes: ImagePicker.MediaTypeOptions.Images,
  363. allowsMultipleSelection: true,
  364. quality: 1,
  365. selectionLimit: 4
  366. });
  367. if (!result.canceled && result.assets) {
  368. const files = result.assets.map((asset) => ({
  369. uri: asset.uri,
  370. type: asset.mimeType ?? 'image',
  371. name: asset.uri ? (asset.uri.split('/').pop() as string) : 'image'
  372. }));
  373. for (const file of files) {
  374. const staticPhoto: any = {
  375. id: new Date().getTime(),
  376. filetype: file.type,
  377. uid: +currentUserId,
  378. name: '',
  379. avatar: null,
  380. isSending: true,
  381. preview: 1,
  382. data: 1
  383. };
  384. setPhotos((prev) => [staticPhoto, ...prev]);
  385. await uploadPhoto(
  386. {
  387. token,
  388. event_id: event.id,
  389. file,
  390. onUploadProgress: (progressEvent) => {
  391. // if (progressEvent.lengthComputable) {
  392. // const progress = Math.round(
  393. // (progressEvent.loaded / (progressEvent.total ?? 100)) * 100
  394. // );
  395. // setUploadProgress((prev) => ({ ...prev, [file!.uri]: progress }));
  396. // }
  397. }
  398. },
  399. {
  400. onSuccess: (result) => {
  401. refetch();
  402. },
  403. onError: () => {
  404. refetch();
  405. }
  406. }
  407. );
  408. }
  409. }
  410. } catch {}
  411. }, [token, event]);
  412. if (!event) return <Loading />;
  413. const handleShare = async () => {
  414. if (!event) return;
  415. try {
  416. // TO DO
  417. const uri = `${API_HOST}/event/${eventUrl}`;
  418. if (uri) {
  419. await Share.open({ url: uri });
  420. }
  421. } catch (error) {
  422. console.error('Error sharing the event url:', error);
  423. }
  424. };
  425. const handleJoinEvent = async () => {
  426. if (!token) {
  427. setIsWarningModalVisible(true);
  428. return;
  429. }
  430. if (event.settings.type !== 1 && event.settings.type !== 4) {
  431. setModalInfo({
  432. visible: true,
  433. type: 'success',
  434. title: 'Success',
  435. buttonTitle: 'OK',
  436. message: `Thank you for joining, we’ll get back to you soon.`,
  437. action: () => {}
  438. });
  439. }
  440. await joinEvent(
  441. { token, id: event.id },
  442. {
  443. onSuccess: () => {
  444. setJoined(1);
  445. refetch();
  446. }
  447. }
  448. );
  449. };
  450. const handleUnjoinEvent = async () => {
  451. await unjoinEvent(
  452. { token, id: event.id },
  453. {
  454. onSuccess: () => {
  455. setJoined(0);
  456. refetch();
  457. }
  458. }
  459. );
  460. };
  461. const handleDeleteFile = async (file: EventAttachments) => {
  462. setModalInfo({
  463. visible: true,
  464. type: 'delete',
  465. title: 'Delete file',
  466. buttonTitle: 'Delete',
  467. message: `Are you sure you want to delete this file?`,
  468. action: async () => {
  469. await deleteFile(
  470. {
  471. token,
  472. id: file.id,
  473. event_id: event.id
  474. },
  475. {
  476. onSuccess: () => {
  477. setMyFiles(myFiles.filter((f) => f.id !== file.id));
  478. }
  479. }
  480. );
  481. }
  482. });
  483. };
  484. const renderItem = ({ item, index }: { item: EventAttachments; index: number }) => {
  485. const totalItems = event.attachments.length;
  486. if (!isExpanded && index === 7 && totalItems > 8) {
  487. return (
  488. <TouchableOpacity
  489. style={{
  490. width: fileWidth,
  491. alignItems: 'center',
  492. gap: 4
  493. }}
  494. onPress={() => {
  495. setIsExpanded(true);
  496. }}
  497. >
  498. <View
  499. style={{
  500. backgroundColor: Colors.FILL_LIGHT,
  501. borderRadius: 8,
  502. alignItems: 'center',
  503. justifyContent: 'center',
  504. height: fileWidth,
  505. width: fileWidth
  506. }}
  507. >
  508. <MaterialCommunityIcons name="dots-horizontal" size={36} color={Colors.DARK_BLUE} />
  509. </View>
  510. </TouchableOpacity>
  511. );
  512. }
  513. return (
  514. <TouchableOpacity
  515. style={{
  516. width: fileWidth,
  517. alignItems: 'center',
  518. gap: 4
  519. }}
  520. onPress={() =>
  521. handlePreviewDocument(
  522. API_HOST + '/webapi/events/get-attachment/' + event.id + '/' + item.id,
  523. event.id + '-' + item.filename
  524. )
  525. }
  526. >
  527. <View
  528. style={{
  529. backgroundColor: Colors.FILL_LIGHT,
  530. borderRadius: 8,
  531. alignItems: 'center',
  532. justifyContent: 'center',
  533. height: fileWidth,
  534. width: fileWidth
  535. }}
  536. >
  537. <MaterialCommunityIcons
  538. name={item.filetype.startsWith('image') ? 'image' : 'file'}
  539. size={36}
  540. color={Colors.DARK_BLUE}
  541. />
  542. </View>
  543. <Text
  544. style={{ fontSize: 12, fontWeight: '600', color: Colors.DARK_BLUE }}
  545. numberOfLines={2}
  546. >
  547. {item.filename}
  548. </Text>
  549. </TouchableOpacity>
  550. );
  551. };
  552. const renderItemFile = ({ item, index }: { item: EventAttachments; index: number }) => {
  553. return (
  554. <TouchableOpacity
  555. style={{
  556. flexDirection: 'row',
  557. alignItems: 'center',
  558. gap: 8,
  559. backgroundColor: Colors.FILL_LIGHT,
  560. flex: 1,
  561. paddingHorizontal: 8,
  562. paddingVertical: 12,
  563. borderRadius: 8
  564. }}
  565. onPress={() => {
  566. handlePreviewDocument(
  567. `${API_HOST}/webapi/events/get-file/${event.id}/${item.id}/?token=${token}`,
  568. item.filename
  569. );
  570. }}
  571. >
  572. <View style={{ gap: 8, flex: 3.5 }}>
  573. <View style={{ flexDirection: 'row', alignItems: 'center', gap: 8, flex: 1 }}>
  574. <FileIcon fill={Colors.DARK_BLUE} height={18} />
  575. <Text style={{ color: Colors.DARK_BLUE, fontSize: 13, fontWeight: '600' }}>
  576. {item.filename}
  577. </Text>
  578. </View>
  579. <Text style={{ color: Colors.TEXT_GRAY, fontSize: 12, fontWeight: '500' }}>
  580. {item.type === 1 ? 'passport' : item.type === 2 ? 'disclaimer' : 'other'}
  581. </Text>
  582. {item.description ? (
  583. <Text style={{ color: Colors.DARK_BLUE, fontSize: 13, fontWeight: '500' }}>
  584. {item.description}
  585. </Text>
  586. ) : null}
  587. </View>
  588. <View style={{ flex: 1 }}>
  589. <TouchableOpacity
  590. style={{
  591. flexDirection: 'row',
  592. alignItems: 'center',
  593. justifyContent: 'center',
  594. gap: 8,
  595. backgroundColor: Colors.RED,
  596. paddingVertical: 8,
  597. paddingHorizontal: 4,
  598. borderRadius: 20
  599. }}
  600. onPress={() => handleDeleteFile(item)}
  601. >
  602. <Text
  603. style={{
  604. color: Colors.WHITE,
  605. fontSize: getFontSize(13),
  606. fontWeight: '700'
  607. }}
  608. >
  609. Delete
  610. </Text>
  611. </TouchableOpacity>
  612. </View>
  613. </TouchableOpacity>
  614. );
  615. };
  616. const formatEventDate = (event: EventData) => {
  617. if (event.date_from && event.date_to) {
  618. if (event.date_tentative) {
  619. const dateFrom = moment(event.date_from, 'YYYY-MM').format('MMM YYYY');
  620. const dateTo = moment(event.date_to, 'YYYY-MM').format('MMM YYYY');
  621. if (dateFrom === dateTo) {
  622. return (
  623. <View>
  624. <Text
  625. style={{
  626. fontSize: getFontSize(12),
  627. fontWeight: '600',
  628. color: Colors.DARK_BLUE
  629. }}
  630. >
  631. {dateFrom}
  632. </Text>
  633. </View>
  634. );
  635. }
  636. return (
  637. <View>
  638. <Text
  639. style={{
  640. fontSize: getFontSize(12),
  641. fontWeight: '600',
  642. color: Colors.DARK_BLUE,
  643. flex: 1
  644. }}
  645. >
  646. {dateFrom}{' '}
  647. </Text>
  648. <Text
  649. style={{
  650. fontSize: getFontSize(12),
  651. fontWeight: '600',
  652. color: Colors.DARK_BLUE,
  653. flex: 1
  654. }}
  655. >
  656. {dateTo}
  657. </Text>
  658. </View>
  659. );
  660. }
  661. return (
  662. <View>
  663. <Text
  664. style={{
  665. fontSize: getFontSize(12),
  666. fontWeight: '600',
  667. color: Colors.DARK_BLUE,
  668. flex: 1
  669. }}
  670. >
  671. {moment(event.settings.date_from, 'YYYY-MM-DD').format('DD MMM YYYY')}{' '}
  672. </Text>
  673. <Text
  674. style={{
  675. fontSize: getFontSize(12),
  676. fontWeight: '600',
  677. color: Colors.DARK_BLUE,
  678. flex: 1
  679. }}
  680. >
  681. {moment(event.settings.date_to, 'YYYY-MM-DD').format('DD MMM YYYY')}
  682. </Text>
  683. </View>
  684. );
  685. } else {
  686. let date = moment(event.date, 'YYYY-MM-DD').format('DD MMM YYYY');
  687. if (event.date_tentative) {
  688. return (
  689. <View>
  690. <Text
  691. style={{
  692. fontSize: getFontSize(12),
  693. fontWeight: '600',
  694. color: Colors.DARK_BLUE
  695. }}
  696. >
  697. {moment(event.date, 'YYYY-MM').format('MMM YYYY')}
  698. </Text>
  699. </View>
  700. );
  701. }
  702. return (
  703. <View>
  704. <Text
  705. style={{
  706. fontSize: getFontSize(12),
  707. fontWeight: '600',
  708. color: Colors.DARK_BLUE
  709. }}
  710. >
  711. {date}
  712. </Text>
  713. {event.time_from && event.time_to ? (
  714. <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
  715. <Text
  716. style={{
  717. fontSize: getFontSize(12),
  718. fontWeight: '600',
  719. color: Colors.DARK_BLUE
  720. }}
  721. >
  722. {event.time_from} - {event.time_to}
  723. </Text>
  724. </View>
  725. ) : event.time ? (
  726. <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
  727. <Text
  728. style={{
  729. fontSize: getFontSize(12),
  730. fontWeight: '600',
  731. color: Colors.DARK_BLUE
  732. }}
  733. >
  734. {event.time}
  735. </Text>
  736. </View>
  737. ) : null}
  738. </View>
  739. );
  740. }
  741. };
  742. const handleSaveFile = async (file: TempFile) => {
  743. setMyTempFiles(() =>
  744. myTempFiles.map((f) => (f.temp_name === file.temp_name ? { ...f, isSending: true } : f))
  745. );
  746. await saveFile(
  747. {
  748. token,
  749. event_id: event.id,
  750. type: file.type,
  751. description: file.description,
  752. filetype: file.filetype,
  753. filename: file.name,
  754. temp_filename: file.temp_name
  755. },
  756. {
  757. onSuccess: () => {
  758. setMyTempFiles(myTempFiles.filter((f) => f.temp_name !== file.temp_name));
  759. refetch();
  760. },
  761. onError: () => {
  762. setMyTempFiles(() =>
  763. myTempFiles.map((f) =>
  764. f.temp_name === file.temp_name ? { ...f, isSending: false } : f
  765. )
  766. );
  767. }
  768. }
  769. );
  770. };
  771. const openMap = () => {
  772. const rawCoords = event.settings.location;
  773. const coords =
  774. typeof rawCoords === 'string' ? JSON.parse(rawCoords.replace(/'/g, '"')) : rawCoords;
  775. const lat = coords.lat;
  776. const lon = coords.lon;
  777. const url =
  778. Platform.OS === 'ios'
  779. ? `http://maps.apple.com/?ll=${lat},${lon}`
  780. : `geo:${lat},${lon}?q=${lat},${lon}`;
  781. const googleMapsUrl = `https://www.google.com/maps/search/?api=1&query=${lat},${lon}`;
  782. Linking.canOpenURL(googleMapsUrl)
  783. .then((supported) => {
  784. if (supported) {
  785. Linking.openURL(googleMapsUrl);
  786. } else {
  787. return Linking.canOpenURL(url).then((supportedFallback) => {
  788. if (supportedFallback) {
  789. Linking.openURL(url);
  790. } else {
  791. navigation.dispatch(
  792. CommonActions.reset({
  793. index: 1,
  794. routes: [
  795. {
  796. name: NAVIGATION_PAGES.IN_APP_MAP_TAB,
  797. state: {
  798. routes: [
  799. {
  800. name: NAVIGATION_PAGES.MAP_TAB,
  801. params: { lat, lon }
  802. }
  803. ]
  804. }
  805. }
  806. ]
  807. })
  808. );
  809. }
  810. });
  811. }
  812. })
  813. .catch((err) => console.error('Error opening event location', err));
  814. };
  815. const photoUrl = API_HOST + '/webapi/events/get-main-photo/' + event.id;
  816. return (
  817. <View style={styles.container}>
  818. <TouchableOpacity
  819. onPress={() => {
  820. navigation.goBack();
  821. }}
  822. style={styles.backButton}
  823. >
  824. <View style={styles.chevronWrapper}>
  825. <ChevronLeft fill={Colors.WHITE} />
  826. </View>
  827. </TouchableOpacity>
  828. <KeyboardAwareScrollView
  829. ref={keyboardAwareScrollViewRef}
  830. showsVerticalScrollIndicator={false}
  831. onScroll={handleScroll}
  832. scrollEventThrottle={16}
  833. >
  834. <ScrollView
  835. ref={scrollViewRef}
  836. contentContainerStyle={{ minHeight: '100%' }}
  837. nestedScrollEnabled={true}
  838. showsVerticalScrollIndicator={false}
  839. removeClippedSubviews={false}
  840. >
  841. {(event.settings.type === 1 || event.settings.type === 4) &&
  842. photosForRegion.length > 0 ? (
  843. <ImageCarousel
  844. photos={photosForRegion as PhotosData[]}
  845. activeIndex={activeIndex}
  846. setActiveIndex={setActiveIndex}
  847. openModal={openModal}
  848. containerStyles={{ marginBottom: 0 }}
  849. />
  850. ) : (
  851. <Image source={{ uri: photoUrl }} style={{ width: '100%', height: 220 }} />
  852. )}
  853. {registrationInfo && (
  854. <View
  855. style={{
  856. position: 'absolute',
  857. width: 'auto',
  858. height: 31,
  859. top: 170,
  860. left: 0,
  861. justifyContent: 'center',
  862. alignItems: 'center',
  863. zIndex: 2,
  864. backgroundColor: registrationInfo.color,
  865. borderTopRightRadius: 4,
  866. borderBottomRightRadius: 4,
  867. paddingRight: 10,
  868. paddingLeft: 16
  869. }}
  870. >
  871. <Text
  872. style={{
  873. textTransform: 'uppercase',
  874. fontSize: getFontSize(16),
  875. fontWeight: '700',
  876. color: Colors.WHITE
  877. }}
  878. >
  879. {registrationInfo.name}
  880. </Text>
  881. </View>
  882. )}
  883. <View style={styles.wrapper}>
  884. <View style={styles.nameContainer}>
  885. <Text style={styles.title}>{event.settings.name}</Text>
  886. <TouchableOpacity
  887. onPress={handleShare}
  888. style={{
  889. alignItems: 'center',
  890. justifyContent: 'center',
  891. paddingLeft: 8,
  892. marginLeft: 4
  893. }}
  894. >
  895. <ShareIcon
  896. width={20}
  897. height={20}
  898. fill={Colors.DARK_BLUE}
  899. style={{ alignSelf: 'center' }}
  900. />
  901. </TouchableOpacity>
  902. </View>
  903. <View style={styles.divider} />
  904. <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
  905. <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4, flex: 1 }}>
  906. {event.settings.type === 1 && event?.flag ? (
  907. <Tooltip
  908. isVisible={tooltipVisible}
  909. content={<Text>{event.country}</Text>}
  910. contentStyle={{ backgroundColor: Colors.FILL_LIGHT }}
  911. placement="top"
  912. onClose={() => setTooltipVisible(false)}
  913. backgroundColor="transparent"
  914. allowChildInteraction={false}
  915. >
  916. <TouchableOpacity onPress={() => setTooltipVisible(true)}>
  917. <Image
  918. source={{ uri: API_HOST + event.flag }}
  919. style={{
  920. width: 20,
  921. height: 20,
  922. borderRadius: 10,
  923. borderWidth: 0.5,
  924. borderColor: Colors.DARK_LIGHT
  925. }}
  926. />
  927. </TouchableOpacity>
  928. </Tooltip>
  929. ) : (
  930. <EarthIcon fill={Colors.DARK_BLUE} height={20} width={20} />
  931. )}
  932. <Text
  933. style={{
  934. fontSize: getFontSize(12),
  935. fontWeight: '600',
  936. color: Colors.DARK_BLUE,
  937. flex: 1
  938. }}
  939. >
  940. {event.address1}
  941. </Text>
  942. </View>
  943. <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4, flex: 1 }}>
  944. <CalendarIcon fill={Colors.DARK_BLUE} height={20} width={20} />
  945. {formatEventDate(event)}
  946. </View>
  947. </View>
  948. <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}>
  949. {event.settings.address2 && (
  950. <TouchableOpacity
  951. onPress={openMap}
  952. disabled={!event.settings.location}
  953. style={{ flexDirection: 'row', alignItems: 'center', gap: 4, flex: 1 }}
  954. >
  955. <LocationIcon fill={Colors.DARK_BLUE} height={20} width={20} />
  956. <Text
  957. style={{
  958. fontSize: getFontSize(12),
  959. fontWeight: '600',
  960. color: Colors.DARK_BLUE,
  961. flex: 1
  962. }}
  963. >
  964. {event.settings.address2}
  965. </Text>
  966. </TouchableOpacity>
  967. )}
  968. {event.settings.registrations_info !== 1 && event.type !== 4 && (
  969. <View style={{ flexDirection: 'row', alignItems: 'center', gap: 4, flex: 1 }}>
  970. <NomadsIcon fill={Colors.DARK_BLUE} height={20} width={20} />
  971. <Text
  972. style={{
  973. fontSize: getFontSize(12),
  974. fontWeight: '600',
  975. color: Colors.DARK_BLUE,
  976. flex: 1
  977. }}
  978. >
  979. {renderSpotsText(event)}
  980. </Text>
  981. </View>
  982. )}
  983. </View>
  984. <View style={styles.stats}>
  985. {event.settings.host_data ? (
  986. <View style={{ gap: 8, flex: 1 }}>
  987. <Text style={[styles.travelSeriesTitle, { fontSize: 12 }]}>Host</Text>
  988. <TouchableOpacity
  989. style={[styles.statItem, { justifyContent: 'flex-start' }]}
  990. onPress={() =>
  991. navigation.navigate(
  992. ...([
  993. NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW,
  994. {
  995. userId: event.settings.host_profile
  996. }
  997. ] as never)
  998. )
  999. }
  1000. disabled={!event.settings.host_profile}
  1001. >
  1002. <View style={styles.userImageContainer}>
  1003. {event.settings.host_data.avatar ? (
  1004. <Image
  1005. source={{
  1006. uri: API_HOST + '/img/avatars/' + event.settings.host_data.avatar
  1007. }}
  1008. style={[styles.userImage, { marginLeft: 0 }]}
  1009. />
  1010. ) : (
  1011. <AvatarWithInitials
  1012. text={
  1013. event.settings.host_data.first_name[0] +
  1014. event.settings.host_data.last_name[0]
  1015. }
  1016. flag={API_HOST + event.settings.host_data?.flag}
  1017. size={28}
  1018. fontSize={12}
  1019. borderColor={Colors.DARK_LIGHT}
  1020. borderWidth={1}
  1021. />
  1022. )}
  1023. <View style={{ justifyContent: 'space-between' }}>
  1024. <Text
  1025. style={{
  1026. fontFamily: 'montserrat-700',
  1027. fontSize: 12,
  1028. color: Colors.DARK_BLUE
  1029. }}
  1030. >
  1031. {event.settings.host_data.first_name}
  1032. </Text>
  1033. <Text
  1034. style={{
  1035. fontFamily: 'montserrat-700',
  1036. fontSize: 12,
  1037. color: Colors.DARK_BLUE
  1038. }}
  1039. >
  1040. {event.settings.host_data.last_name}
  1041. </Text>
  1042. </View>
  1043. </View>
  1044. </TouchableOpacity>
  1045. </View>
  1046. ) : (
  1047. <View style={[styles.statItem, { justifyContent: 'flex-start' }]} />
  1048. )}
  1049. {(filteredParticipants.length > 0 && event.type !== 4) ||
  1050. (filteredInterested.length === 0 &&
  1051. event.type === 4 &&
  1052. filteredParticipants.length > 0) ? (
  1053. <View style={{ gap: 8, flex: 1 }}>
  1054. <Text style={[styles.travelSeriesTitle, { fontSize: 12 }]}>
  1055. Going{` (${filteredParticipants.length})`}
  1056. </Text>
  1057. <View style={[styles.statItem, { justifyContent: 'flex-start' }]}>
  1058. <View style={styles.userImageContainer}>
  1059. {(filteredParticipants.length > maxVisibleParticipants
  1060. ? filteredParticipants.slice(0, maxVisibleParticipants - 1)
  1061. : filteredParticipants
  1062. ).map((user, index) => (
  1063. <Tooltip
  1064. isVisible={tooltipUser === index}
  1065. content={
  1066. <TouchableOpacity
  1067. onPress={() => {
  1068. setTooltipUser(null);
  1069. navigation.navigate(
  1070. ...([
  1071. NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW,
  1072. { userId: user.uid }
  1073. ] as never)
  1074. );
  1075. }}
  1076. >
  1077. <Text>{user.name}</Text>
  1078. </TouchableOpacity>
  1079. }
  1080. contentStyle={{ backgroundColor: Colors.FILL_LIGHT }}
  1081. placement="top"
  1082. onClose={() => setTooltipUser(null)}
  1083. key={index}
  1084. backgroundColor="transparent"
  1085. >
  1086. <TouchableOpacity
  1087. style={index !== 0 ? { marginLeft: -10 } : {}}
  1088. onPress={() => setTooltipUser(index)}
  1089. >
  1090. {user.avatar ? (
  1091. <Image
  1092. key={index}
  1093. source={{ uri: API_HOST + user.avatar }}
  1094. style={[styles.userImage]}
  1095. />
  1096. ) : (
  1097. <AvatarWithInitials
  1098. text={
  1099. user.name?.split(' ')[0][0] + (user.name?.split(' ')[1][0] ?? '')
  1100. }
  1101. flag={API_HOST + user?.flag}
  1102. size={28}
  1103. fontSize={12}
  1104. borderColor={Colors.DARK_LIGHT}
  1105. borderWidth={1}
  1106. />
  1107. )}
  1108. </TouchableOpacity>
  1109. </Tooltip>
  1110. ))}
  1111. <TouchableOpacity
  1112. style={styles.userCountContainer}
  1113. onPress={() =>
  1114. navigation.navigate(
  1115. ...([
  1116. NAVIGATION_PAGES.PARTICIPANTS_LIST,
  1117. {
  1118. participants:
  1119. event.type === 4
  1120. ? event.participants_approved_full_data
  1121. : event.participants_full_data,
  1122. eventId: event.id,
  1123. isHost: event.settings.host_profile === +currentUserId,
  1124. isTrip: event.type === 4
  1125. }
  1126. ] as never)
  1127. )
  1128. }
  1129. >
  1130. <View style={styles.dots}></View>
  1131. <View style={styles.dots}></View>
  1132. <View style={styles.dots}></View>
  1133. </TouchableOpacity>
  1134. </View>
  1135. </View>
  1136. </View>
  1137. ) : null}
  1138. {filteredParticipants.length === 0 &&
  1139. event.type === 4 &&
  1140. filteredInterested.length > 0 ? (
  1141. <View style={{ gap: 8, flex: 1 }}>
  1142. <Text style={[styles.travelSeriesTitle, { fontSize: 12 }]}>
  1143. Interested{` (${filteredInterested.length})`}
  1144. </Text>
  1145. <View style={[styles.statItem, { justifyContent: 'flex-start' }]}>
  1146. <View style={styles.userImageContainer}>
  1147. {(filteredInterested.length > maxVisibleParticipants
  1148. ? filteredInterested.slice(0, maxVisibleParticipants - 1)
  1149. : filteredInterested
  1150. ).map((user, index) => (
  1151. <Tooltip
  1152. isVisible={tooltipInterested === index}
  1153. content={
  1154. <TouchableOpacity
  1155. onPress={() => {
  1156. setTooltipInterested(null);
  1157. navigation.navigate(
  1158. ...([
  1159. NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW,
  1160. { userId: user.uid }
  1161. ] as never)
  1162. );
  1163. }}
  1164. >
  1165. <Text>{user.name}</Text>
  1166. </TouchableOpacity>
  1167. }
  1168. contentStyle={{ backgroundColor: Colors.FILL_LIGHT }}
  1169. placement="top"
  1170. onClose={() => setTooltipInterested(null)}
  1171. key={index}
  1172. backgroundColor="transparent"
  1173. >
  1174. <TouchableOpacity
  1175. style={index !== 0 ? { marginLeft: -10 } : {}}
  1176. onPress={() => setTooltipInterested(index)}
  1177. >
  1178. {user.avatar ? (
  1179. <Image
  1180. key={index}
  1181. source={{ uri: API_HOST + user.avatar }}
  1182. style={[styles.userImage]}
  1183. />
  1184. ) : (
  1185. <AvatarWithInitials
  1186. text={
  1187. user.name?.split(' ')[0][0] + (user.name?.split(' ')[1][0] ?? '')
  1188. }
  1189. flag={API_HOST + user?.flag}
  1190. size={28}
  1191. fontSize={12}
  1192. borderColor={Colors.DARK_LIGHT}
  1193. borderWidth={1}
  1194. />
  1195. )}
  1196. </TouchableOpacity>
  1197. </Tooltip>
  1198. ))}
  1199. <TouchableOpacity
  1200. style={styles.userCountContainer}
  1201. onPress={() =>
  1202. navigation.navigate(
  1203. ...([
  1204. NAVIGATION_PAGES.PARTICIPANTS_LIST,
  1205. {
  1206. participants: event.participants_full_data,
  1207. eventId: event.id,
  1208. isHost: event.settings.host_profile === +currentUserId,
  1209. interested: true,
  1210. isTrip: event.type === 4
  1211. }
  1212. ] as never)
  1213. )
  1214. }
  1215. >
  1216. <View style={styles.dots}></View>
  1217. <View style={styles.dots}></View>
  1218. <View style={styles.dots}></View>
  1219. </TouchableOpacity>
  1220. </View>
  1221. </View>
  1222. </View>
  1223. ) : null}
  1224. </View>
  1225. {filteredInterested.length > 0 &&
  1226. filteredParticipants.length > 0 &&
  1227. event.type === 4 && (
  1228. <View style={styles.stats}>
  1229. {filteredInterested.length > 0 ? (
  1230. <View style={{ gap: 8, flex: 1 }}>
  1231. <Text style={[styles.travelSeriesTitle, { fontSize: 12 }]}>
  1232. Interested{` (${filteredInterested.length})`}
  1233. </Text>
  1234. <View style={[styles.statItem, { justifyContent: 'flex-start' }]}>
  1235. <View style={styles.userImageContainer}>
  1236. {(filteredInterested.length > maxVisibleParticipants
  1237. ? filteredInterested.slice(0, maxVisibleParticipants - 1)
  1238. : filteredInterested
  1239. ).map((user, index) => (
  1240. <Tooltip
  1241. isVisible={tooltipInterested === index}
  1242. content={
  1243. <TouchableOpacity
  1244. onPress={() => {
  1245. setTooltipInterested(null);
  1246. navigation.navigate(
  1247. ...([
  1248. NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW,
  1249. { userId: user.uid }
  1250. ] as never)
  1251. );
  1252. }}
  1253. >
  1254. <Text>{user.name}</Text>
  1255. </TouchableOpacity>
  1256. }
  1257. contentStyle={{ backgroundColor: Colors.FILL_LIGHT }}
  1258. placement="top"
  1259. onClose={() => setTooltipInterested(null)}
  1260. key={index}
  1261. backgroundColor="transparent"
  1262. >
  1263. <TouchableOpacity
  1264. style={index !== 0 ? { marginLeft: -10 } : {}}
  1265. onPress={() => setTooltipInterested(index)}
  1266. >
  1267. {user.avatar ? (
  1268. <Image
  1269. key={index}
  1270. source={{ uri: API_HOST + user.avatar }}
  1271. style={[styles.userImage]}
  1272. />
  1273. ) : (
  1274. <AvatarWithInitials
  1275. text={
  1276. user.name?.split(' ')[0][0] +
  1277. (user.name?.split(' ')[1][0] ?? '')
  1278. }
  1279. flag={API_HOST + user?.flag}
  1280. size={28}
  1281. fontSize={12}
  1282. borderColor={Colors.DARK_LIGHT}
  1283. borderWidth={1}
  1284. />
  1285. )}
  1286. </TouchableOpacity>
  1287. </Tooltip>
  1288. ))}
  1289. <TouchableOpacity
  1290. style={styles.userCountContainer}
  1291. onPress={() =>
  1292. navigation.navigate(
  1293. ...([
  1294. NAVIGATION_PAGES.PARTICIPANTS_LIST,
  1295. {
  1296. participants: event.participants_full_data,
  1297. eventId: event.id,
  1298. isHost: event.settings.host_profile === +currentUserId,
  1299. interested: true,
  1300. isTrip: event.type === 4
  1301. }
  1302. ] as never)
  1303. )
  1304. }
  1305. >
  1306. <View style={styles.dots}></View>
  1307. <View style={styles.dots}></View>
  1308. <View style={styles.dots}></View>
  1309. </TouchableOpacity>
  1310. </View>
  1311. </View>
  1312. </View>
  1313. ) : (
  1314. <View style={[styles.statItem, { justifyContent: 'flex-start' }]} />
  1315. )}
  1316. {filteredParticipants.length > 0 ? (
  1317. <View style={{ gap: 8, flex: 1 }}>
  1318. <Text style={[styles.travelSeriesTitle, { fontSize: 12 }]}>
  1319. Going{` (${filteredParticipants.length})`}
  1320. </Text>
  1321. <View style={[styles.statItem, { justifyContent: 'flex-start' }]}>
  1322. <View style={styles.userImageContainer}>
  1323. {(filteredParticipants.length > maxVisibleParticipants
  1324. ? filteredParticipants.slice(0, maxVisibleParticipants - 1)
  1325. : filteredParticipants
  1326. ).map((user, index) => (
  1327. <Tooltip
  1328. isVisible={tooltipUser === index}
  1329. content={
  1330. <TouchableOpacity
  1331. onPress={() => {
  1332. setTooltipUser(null);
  1333. navigation.navigate(
  1334. ...([
  1335. NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW,
  1336. { userId: user.uid }
  1337. ] as never)
  1338. );
  1339. }}
  1340. >
  1341. <Text>{user.name}</Text>
  1342. </TouchableOpacity>
  1343. }
  1344. contentStyle={{ backgroundColor: Colors.FILL_LIGHT }}
  1345. placement="top"
  1346. onClose={() => setTooltipUser(null)}
  1347. key={index}
  1348. backgroundColor="transparent"
  1349. >
  1350. <TouchableOpacity
  1351. style={index !== 0 ? { marginLeft: -10 } : {}}
  1352. onPress={() => setTooltipUser(index)}
  1353. >
  1354. {user.avatar ? (
  1355. <Image
  1356. key={index}
  1357. source={{ uri: API_HOST + user.avatar }}
  1358. style={[styles.userImage]}
  1359. />
  1360. ) : (
  1361. <AvatarWithInitials
  1362. text={
  1363. user.name?.split(' ')[0][0] +
  1364. (user.name?.split(' ')[1][0] ?? '')
  1365. }
  1366. flag={API_HOST + user?.flag}
  1367. size={28}
  1368. fontSize={12}
  1369. borderColor={Colors.DARK_LIGHT}
  1370. borderWidth={1}
  1371. />
  1372. )}
  1373. </TouchableOpacity>
  1374. </Tooltip>
  1375. ))}
  1376. <TouchableOpacity
  1377. style={styles.userCountContainer}
  1378. onPress={() =>
  1379. navigation.navigate(
  1380. ...([
  1381. NAVIGATION_PAGES.PARTICIPANTS_LIST,
  1382. {
  1383. participants:
  1384. event.type === 4
  1385. ? event.participants_approved_full_data
  1386. : event.participants_full_data,
  1387. eventId: event.id,
  1388. isHost: event.settings.host_profile === +currentUserId,
  1389. isTrip: event.type === 4
  1390. }
  1391. ] as never)
  1392. )
  1393. }
  1394. >
  1395. <View style={styles.dots}></View>
  1396. <View style={styles.dots}></View>
  1397. <View style={styles.dots}></View>
  1398. </TouchableOpacity>
  1399. </View>
  1400. </View>
  1401. </View>
  1402. ) : (
  1403. <View style={[styles.statItem, { justifyContent: 'flex-end' }]} />
  1404. )}
  1405. </View>
  1406. )}
  1407. {/* TO DO */}
  1408. {event.settings.host_profile === +currentUserId &&
  1409. (event.settings.type === 1 || event.settings.type === 4) &&
  1410. !event.archived ? (
  1411. <TouchableOpacity
  1412. style={{
  1413. flexDirection: 'row',
  1414. alignItems: 'center',
  1415. justifyContent: 'center',
  1416. paddingVertical: 8,
  1417. paddingHorizontal: 12,
  1418. borderRadius: 20,
  1419. backgroundColor: Colors.ORANGE,
  1420. gap: 6,
  1421. borderWidth: 1,
  1422. borderColor: Colors.ORANGE
  1423. }}
  1424. onPress={() =>
  1425. getForEditing(
  1426. { token, event_id: event.id },
  1427. {
  1428. onSuccess: (res) => {
  1429. event.settings.type === 4
  1430. ? navigation.navigate(
  1431. ...([
  1432. NAVIGATION_PAGES.CREATE_SHARED_TRIP,
  1433. {
  1434. eventId: event.id,
  1435. event: res,
  1436. full: event.full
  1437. }
  1438. ] as never)
  1439. )
  1440. : navigation.navigate(
  1441. ...([
  1442. NAVIGATION_PAGES.CREATE_EVENT,
  1443. {
  1444. eventId: event.id,
  1445. event: res
  1446. }
  1447. ] as never)
  1448. );
  1449. }
  1450. }
  1451. )
  1452. }
  1453. >
  1454. <EditSvg fill={Colors.WHITE} width={16} height={16} />
  1455. <Text
  1456. style={{
  1457. color: Colors.WHITE,
  1458. fontSize: getFontSize(14),
  1459. fontFamily: 'montserrat-700',
  1460. textTransform: 'uppercase'
  1461. }}
  1462. >
  1463. Edit
  1464. </Text>
  1465. </TouchableOpacity>
  1466. ) : null}
  1467. {event.settings.host_profile === +currentUserId ||
  1468. event.archived === 1 ? null : joined ? (
  1469. <View style={{ flexDirection: 'row', gap: 8 }}>
  1470. <TouchableOpacity
  1471. style={{
  1472. flex: 1,
  1473. flexDirection: 'row',
  1474. alignItems: 'center',
  1475. justifyContent: 'center',
  1476. paddingVertical: 8,
  1477. paddingHorizontal: 12,
  1478. borderRadius: 20,
  1479. backgroundColor: Colors.WHITE,
  1480. gap: 6,
  1481. borderWidth: 1,
  1482. borderColor: Colors.DARK_BLUE
  1483. }}
  1484. onPress={handleUnjoinEvent}
  1485. >
  1486. <CalendarCrossedIcon fill={Colors.DARK_BLUE} width={16} height={16} />
  1487. <Text
  1488. style={{
  1489. color: Colors.DARK_BLUE,
  1490. fontSize: getFontSize(14),
  1491. fontFamily: 'montserrat-700'
  1492. }}
  1493. >
  1494. Cancel
  1495. </Text>
  1496. </TouchableOpacity>
  1497. {event.settings?.chat_token && (
  1498. <TouchableOpacity
  1499. style={{
  1500. flex: 1,
  1501. flexDirection: 'row',
  1502. alignItems: 'center',
  1503. justifyContent: 'center',
  1504. paddingVertical: 8,
  1505. paddingHorizontal: 12,
  1506. borderRadius: 20,
  1507. backgroundColor: Colors.ORANGE,
  1508. gap: 6,
  1509. borderWidth: 1,
  1510. borderColor: Colors.ORANGE
  1511. }}
  1512. onPress={() =>
  1513. navigation.dispatch(
  1514. CommonActions.reset({
  1515. index: 1,
  1516. routes: [
  1517. {
  1518. name: 'DrawerApp',
  1519. state: {
  1520. routes: [
  1521. {
  1522. name: NAVIGATION_PAGES.IN_APP_MESSAGES_TAB,
  1523. state: {
  1524. routes: [
  1525. { name: NAVIGATION_PAGES.CHATS_LIST },
  1526. {
  1527. name: NAVIGATION_PAGES.GROUP_CHAT,
  1528. params: { group_token: event.settings?.chat_token }
  1529. }
  1530. ]
  1531. }
  1532. }
  1533. ]
  1534. }
  1535. }
  1536. ]
  1537. })
  1538. )
  1539. }
  1540. >
  1541. <ChatIcon fill={Colors.WHITE} width={16} height={16} />
  1542. <Text
  1543. style={{
  1544. color: Colors.WHITE,
  1545. fontSize: getFontSize(14),
  1546. fontFamily: 'montserrat-700'
  1547. }}
  1548. >
  1549. Chat
  1550. </Text>
  1551. </TouchableOpacity>
  1552. )}
  1553. </View>
  1554. ) : !event.full && !event.closed ? (
  1555. <TouchableOpacity
  1556. style={{
  1557. flexDirection: 'row',
  1558. alignItems: 'center',
  1559. justifyContent: 'center',
  1560. paddingVertical: 8,
  1561. paddingHorizontal: 12,
  1562. borderRadius: 20,
  1563. backgroundColor: Colors.ORANGE,
  1564. gap: 6,
  1565. borderWidth: 1,
  1566. borderColor: Colors.ORANGE
  1567. }}
  1568. onPress={handleJoinEvent}
  1569. >
  1570. {event.settings.type === 1 ? (
  1571. <>
  1572. <GigtIcon fill={Colors.WHITE} width={16} height={16} />
  1573. <Text
  1574. style={{
  1575. color: Colors.WHITE,
  1576. fontSize: getFontSize(14),
  1577. fontFamily: 'montserrat-700',
  1578. textTransform: 'uppercase'
  1579. }}
  1580. >
  1581. Going
  1582. </Text>
  1583. </>
  1584. ) : (
  1585. <>
  1586. <CalendarCheckIcon fill={Colors.WHITE} width={16} height={16} />
  1587. <Text
  1588. style={{
  1589. color: Colors.WHITE,
  1590. fontSize: getFontSize(14),
  1591. fontFamily: 'montserrat-700',
  1592. textTransform: 'uppercase'
  1593. }}
  1594. >
  1595. Interested
  1596. </Text>
  1597. </>
  1598. )}
  1599. </TouchableOpacity>
  1600. ) : null}
  1601. {event.type === 4 ? (
  1602. <TouchableOpacity
  1603. onPress={() => setToolTipVisible(true)}
  1604. style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}
  1605. >
  1606. <Tooltip
  1607. isVisible={toolTipVisible}
  1608. content={
  1609. <Text style={{ fontSize: 12, color: Colors.DARK_BLUE }}>
  1610. Disclaimer: This trip has been created and published by a community member.
  1611. NomadMania is not the organizer and is not involved in planning, managing, or
  1612. overseeing this trip. We take no responsibility for its content, safety, or
  1613. execution. Please exercise your own judgment when joining.
  1614. </Text>
  1615. }
  1616. contentStyle={{ backgroundColor: Colors.WHITE }}
  1617. placement="bottom"
  1618. onClose={() => setToolTipVisible(false)}
  1619. backgroundColor="transparent"
  1620. allowChildInteraction={false}
  1621. >
  1622. <TouchableOpacity
  1623. onPress={() => setToolTipVisible(true)}
  1624. hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
  1625. >
  1626. <InfoIcon fill={Colors.DARK_BLUE} width={16} height={16} />
  1627. </TouchableOpacity>
  1628. </Tooltip>
  1629. <Text style={{ fontSize: 12, color: Colors.DARK_BLUE, fontWeight: '600' }}>
  1630. Disclaimer [read more]
  1631. </Text>
  1632. </TouchableOpacity>
  1633. ) : null}
  1634. <View style={[styles.divider]} />
  1635. {event.settings.details && event.settings.details.length ? (
  1636. <View style={{ gap: 8 }}>
  1637. <Text style={styles.travelSeriesTitle}>Details</Text>
  1638. <WebDisplay html={event.settings.details} />
  1639. </View>
  1640. ) : null}
  1641. {event.attachments.length > 0 ? (
  1642. <View style={{ gap: 16 }}>
  1643. <Text style={styles.travelSeriesTitle}>Attachments</Text>
  1644. <FlatList
  1645. data={isExpanded ? event.attachments : event.attachments.slice(0, 8)}
  1646. renderItem={renderItem}
  1647. keyExtractor={(item) => item.id.toString()}
  1648. numColumns={4}
  1649. columnWrapperStyle={{
  1650. justifyContent: 'flex-start',
  1651. gap: 12
  1652. }}
  1653. contentContainerStyle={{
  1654. gap: 8,
  1655. alignSelf: 'center'
  1656. }}
  1657. showsVerticalScrollIndicator={false}
  1658. scrollEnabled={false}
  1659. />
  1660. </View>
  1661. ) : null}
  1662. {(photos && photos.length) ||
  1663. (event.joined && event.settings.participants_can_add_photos) ? (
  1664. <View style={{ gap: 16 }}>
  1665. <View
  1666. style={{
  1667. flexDirection: 'row',
  1668. justifyContent: 'space-between',
  1669. alignItems: 'center'
  1670. }}
  1671. >
  1672. <Text style={styles.travelSeriesTitle}>Photos</Text>
  1673. {event.settings.participants_can_add_photos && event.joined ? (
  1674. <TouchableOpacity
  1675. style={{
  1676. flexDirection: 'row',
  1677. backgroundColor: Colors.ORANGE,
  1678. gap: 6,
  1679. alignItems: 'center',
  1680. justifyContent: 'center',
  1681. paddingVertical: 7,
  1682. paddingHorizontal: 12,
  1683. borderRadius: 20
  1684. }}
  1685. onPress={handleUploadPhoto}
  1686. >
  1687. <Text
  1688. style={{
  1689. fontSize: getFontSize(13),
  1690. fontWeight: '700',
  1691. color: Colors.WHITE
  1692. }}
  1693. >
  1694. Add
  1695. </Text>
  1696. <ImageIcon fill={Colors.WHITE} width={18} height={18} />
  1697. </TouchableOpacity>
  1698. ) : null}
  1699. </View>
  1700. {photos && photos.length > 0 ? (
  1701. <PhotoItem photos={photos} eventId={event.id} photosLeft={event.photos_left} />
  1702. ) : null}
  1703. </View>
  1704. ) : null}
  1705. {(event.files && event.files.length) ||
  1706. (event.joined && event.settings.participants_can_add_files) ? (
  1707. <View style={{ gap: 16, paddingBottom: 16 }}>
  1708. <View
  1709. style={{
  1710. flexDirection: 'row',
  1711. justifyContent: 'space-between',
  1712. alignItems: 'center'
  1713. }}
  1714. >
  1715. <Text style={styles.travelSeriesTitle}>My files</Text>
  1716. {event.settings.participants_can_add_files && event.joined ? (
  1717. <TouchableOpacity
  1718. style={{
  1719. flexDirection: 'row',
  1720. backgroundColor: Colors.ORANGE,
  1721. gap: 6,
  1722. alignItems: 'center',
  1723. justifyContent: 'center',
  1724. paddingVertical: 7,
  1725. paddingHorizontal: 12,
  1726. borderRadius: 20
  1727. }}
  1728. onPress={handleUploadFile}
  1729. >
  1730. <Text
  1731. style={{
  1732. fontSize: getFontSize(13),
  1733. fontWeight: '700',
  1734. color: Colors.WHITE
  1735. }}
  1736. >
  1737. Add
  1738. </Text>
  1739. <FileIcon fill={Colors.WHITE} height={18} />
  1740. </TouchableOpacity>
  1741. ) : null}
  1742. </View>
  1743. {isUploading && (
  1744. <View
  1745. style={{
  1746. alignItems: 'center',
  1747. justifyContent: 'center'
  1748. }}
  1749. >
  1750. <Progress.CircleSnail
  1751. borderWidth={0}
  1752. color={Colors.DARK_BLUE}
  1753. unfilledColor="rgba(0, 0, 0, 0.1)"
  1754. />
  1755. </View>
  1756. )}
  1757. {myTempFiles && myTempFiles.length
  1758. ? myTempFiles.map((file) => {
  1759. return (
  1760. <View
  1761. key={file.temp_name}
  1762. style={{
  1763. flexDirection: 'row',
  1764. alignItems: 'center',
  1765. gap: 8,
  1766. backgroundColor: Colors.FILL_LIGHT,
  1767. flex: 1,
  1768. paddingHorizontal: 8,
  1769. paddingVertical: 12,
  1770. borderRadius: 8
  1771. }}
  1772. >
  1773. <View style={{ gap: 8, flex: 3 }}>
  1774. <View
  1775. style={{
  1776. flexDirection: 'row',
  1777. alignItems: 'center',
  1778. gap: 8,
  1779. flex: 1
  1780. }}
  1781. >
  1782. <FileIcon fill={Colors.DARK_BLUE} height={18} />
  1783. <Text
  1784. style={{ color: Colors.DARK_BLUE, fontSize: 13, fontWeight: '500' }}
  1785. >
  1786. {file.name}
  1787. </Text>
  1788. </View>
  1789. <Input
  1790. height={36}
  1791. backgroundColor={Colors.WHITE}
  1792. placeholder="Add comment here"
  1793. multiline={true}
  1794. onChange={(text) => {
  1795. setMyTempFiles(() =>
  1796. myTempFiles.map((f) =>
  1797. f.temp_name === file.temp_name ? { ...f, description: text } : f
  1798. )
  1799. );
  1800. }}
  1801. />
  1802. <Dropdown
  1803. style={{
  1804. height: 36,
  1805. backgroundColor: Colors.WHITE,
  1806. borderRadius: 4,
  1807. paddingHorizontal: 8
  1808. }}
  1809. placeholderStyle={{
  1810. fontSize: 14,
  1811. color: Colors.DARK_BLUE,
  1812. fontWeight: '500'
  1813. }}
  1814. selectedTextStyle={{
  1815. fontSize: 14,
  1816. color: Colors.DARK_BLUE,
  1817. fontWeight: '500'
  1818. }}
  1819. data={[
  1820. { label: 'passport', value: 1 },
  1821. { label: 'disclaimer', value: 2 },
  1822. { label: 'other', value: 3 }
  1823. ]}
  1824. labelField="label"
  1825. valueField="value"
  1826. value={file.type}
  1827. placeholder="First visit"
  1828. onChange={(item: { value: 1 | 2 | 3 }) => {
  1829. setMyTempFiles(() =>
  1830. myTempFiles.map((f) =>
  1831. f.temp_name === file.temp_name ? { ...f, type: item.value } : f
  1832. )
  1833. );
  1834. }}
  1835. containerStyle={{ borderRadius: 4 }}
  1836. renderItem={(item) => (
  1837. <View style={{ paddingVertical: 12, paddingHorizontal: 16 }}>
  1838. <Text
  1839. style={{
  1840. fontSize: 14,
  1841. color: Colors.DARK_BLUE,
  1842. fontWeight: '500'
  1843. }}
  1844. >
  1845. {item.label}
  1846. </Text>
  1847. </View>
  1848. )}
  1849. />
  1850. </View>
  1851. <View style={{ flex: 1 }}>
  1852. <TouchableOpacity
  1853. style={{
  1854. flexDirection: 'row',
  1855. alignItems: 'center',
  1856. justifyContent: 'center',
  1857. gap: 8,
  1858. backgroundColor: Colors.DARK_BLUE,
  1859. paddingVertical: 8,
  1860. paddingHorizontal: 4,
  1861. borderRadius: 20
  1862. }}
  1863. onPress={() => handleSaveFile(file)}
  1864. disabled={file.isSending}
  1865. >
  1866. {file.isSending ? (
  1867. <View>
  1868. <ActivityIndicator
  1869. size={16}
  1870. color={Colors.WHITE}
  1871. style={{ transform: 'scale(0.9)' }}
  1872. />
  1873. </View>
  1874. ) : (
  1875. <Text
  1876. style={{
  1877. color: Colors.WHITE,
  1878. fontSize: getFontSize(13),
  1879. fontWeight: '700'
  1880. }}
  1881. >
  1882. Save
  1883. </Text>
  1884. )}
  1885. </TouchableOpacity>
  1886. </View>
  1887. </View>
  1888. );
  1889. })
  1890. : null}
  1891. <FlatList
  1892. data={myFiles}
  1893. renderItem={renderItemFile}
  1894. keyExtractor={(item) => item.id.toString()}
  1895. contentContainerStyle={{ gap: 8 }}
  1896. showsVerticalScrollIndicator={false}
  1897. scrollEnabled={false}
  1898. />
  1899. </View>
  1900. ) : null}
  1901. </View>
  1902. </ScrollView>
  1903. </KeyboardAwareScrollView>
  1904. <WarningModal
  1905. type={modalInfo.type}
  1906. isVisible={modalInfo.visible}
  1907. buttonTitle={modalInfo.buttonTitle}
  1908. message={modalInfo.message}
  1909. action={modalInfo.action}
  1910. onClose={() => setModalInfo({ ...modalInfo, visible: false })}
  1911. title={modalInfo.title}
  1912. />
  1913. <ImageView
  1914. images={photosForRegion}
  1915. imageIndex={currentImageIndex}
  1916. visible={isImageModalVisible}
  1917. onRequestClose={() => setIsImageModalVisible(false)}
  1918. backgroundColor={Colors.DARK_BLUE}
  1919. onImageIndexChange={setActiveIndex}
  1920. />
  1921. <WarningModal
  1922. type={'unauthorized'}
  1923. isVisible={isWarningModalVisible}
  1924. onClose={() => setIsWarningModalVisible(false)}
  1925. />
  1926. {showScrollToTop && (
  1927. <Animated.View
  1928. style={{
  1929. position: 'absolute',
  1930. bottom: 20,
  1931. right: 20,
  1932. opacity: scrollToTopOpacity,
  1933. zIndex: 1000
  1934. }}
  1935. >
  1936. <TouchableOpacity
  1937. onPress={scrollToTop}
  1938. style={{
  1939. backgroundColor: Colors.DARK_BLUE,
  1940. width: 40,
  1941. height: 40,
  1942. borderRadius: 20,
  1943. justifyContent: 'center',
  1944. alignItems: 'center',
  1945. shadowColor: Colors.DARK_BLUE,
  1946. shadowOffset: {
  1947. width: 0,
  1948. height: 2
  1949. },
  1950. shadowOpacity: 0.25,
  1951. shadowRadius: 3,
  1952. elevation: 5
  1953. }}
  1954. >
  1955. <MaterialCommunityIcons name="chevron-up" size={24} color={Colors.WHITE} />
  1956. </TouchableOpacity>
  1957. </Animated.View>
  1958. )}
  1959. </View>
  1960. );
  1961. };
  1962. const iframeModel = HTMLElementModel.fromCustomModel({
  1963. tagName: 'iframe',
  1964. contentModel: 'block' as any
  1965. });
  1966. const WebDisplay = React.memo(function WebDisplay({ html }: { html: string }) {
  1967. const { width: windowWidth } = useWindowDimensions();
  1968. const contentWidth = windowWidth * 0.9;
  1969. const token = storage.get('token', StoreType.STRING) as string;
  1970. const processedHtml = React.useMemo(() => {
  1971. let updatedHtml = html;
  1972. const hrefRegex = /href="((?!http)[^"]+)"/g;
  1973. const imgSrcRegex = /src="((?:\.{0,2}\/)*[^":]+)"/g;
  1974. const normalizePath = (path: string): string => {
  1975. const segments = path.split('/').filter(Boolean);
  1976. const resolved: string[] = [];
  1977. for (const segment of segments) {
  1978. if (segment === '..') resolved.pop();
  1979. else if (segment !== '.') resolved.push(segment);
  1980. }
  1981. return '/' + resolved.join('/');
  1982. };
  1983. updatedHtml = updatedHtml
  1984. .replace(hrefRegex, (match, rawPath) => {
  1985. const normalizedPath = normalizePath(rawPath);
  1986. const fullUrl = `${API_HOST}${normalizedPath}`;
  1987. if (normalizedPath.includes('shop')) {
  1988. const separator = fullUrl.includes('?') ? '&' : '?';
  1989. return `href="${fullUrl}${separator}token=${encodeURIComponent(token)}"`;
  1990. }
  1991. return `href="${fullUrl}"`;
  1992. })
  1993. .replace(imgSrcRegex, (_match, rawSrc) => {
  1994. const normalizedImg = normalizePath(rawSrc);
  1995. return `src="${API_HOST}${normalizedImg}"`;
  1996. });
  1997. return updatedHtml;
  1998. }, [html, token]);
  1999. const renderers = {
  2000. iframe: ({ tnode }: { tnode: TNode }) => {
  2001. const src = tnode.attributes.src || '';
  2002. const width = contentWidth;
  2003. const height = Number(tnode.attributes.height) || 250;
  2004. return (
  2005. <WebView
  2006. source={{ uri: src }}
  2007. style={{ width, height }}
  2008. javaScriptEnabled
  2009. domStorageEnabled
  2010. startInLoadingState
  2011. scalesPageToFit
  2012. />
  2013. );
  2014. }
  2015. };
  2016. const customHTMLElementModels = {
  2017. iframe: iframeModel
  2018. };
  2019. return (
  2020. <RenderHtml
  2021. contentWidth={contentWidth}
  2022. source={{ html: processedHtml }}
  2023. customHTMLElementModels={customHTMLElementModels}
  2024. renderers={renderers}
  2025. />
  2026. );
  2027. });
  2028. export default EventScreen;