index.tsx 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266
  1. import React, { useEffect, useRef } from 'react';
  2. import { View, Image, Text, TouchableOpacity, Platform, Share, Pressable } from 'react-native';
  3. import { styles } from './styles';
  4. import { Colors } from 'src/theme';
  5. import CheckSvg from 'assets/icons/mark.svg';
  6. import ShareIcon from 'assets/icons/share.svg';
  7. import * as MapLibreRN from '@maplibre/maplibre-react-native';
  8. import { API_HOST } from 'src/constants';
  9. import { NAVIGATION_PAGES } from 'src/types';
  10. import { useNavigation } from '@react-navigation/native';
  11. import { usePremiumGuard } from 'src/utils/usePremiumGuard';
  12. const MarkerItem = ({
  13. marker,
  14. toggleSeries,
  15. token,
  16. isPremium,
  17. setPremiumModalVisible,
  18. calloutPressRef
  19. }: {
  20. marker: any;
  21. toggleSeries: (item: any) => void;
  22. token: string;
  23. isPremium: boolean;
  24. setPremiumModalVisible: (premium: boolean) => void;
  25. calloutPressRef?: React.MutableRefObject<boolean>;
  26. }) => {
  27. const navigation = useNavigation();
  28. const { guardFeature } = usePremiumGuard();
  29. if (!marker || !marker.coordinates) {
  30. return null;
  31. }
  32. function formatNumber(value?: number | null) {
  33. const number = typeof value === 'number' && !isNaN(value) ? value : 0;
  34. if (number >= 1000 && number < 10000) {
  35. return (number / 1000).toFixed(1) + 'k';
  36. } else if (number >= 10000) {
  37. return (number / 1000).toFixed(0) + 'k';
  38. }
  39. return number.toString();
  40. }
  41. const formattedCount = formatNumber(marker.no_visited);
  42. const parsedAvatars =
  43. typeof marker.avatars === 'string' ? JSON.parse(marker.avatars) : marker.avatars;
  44. return (
  45. <>
  46. {Platform.OS === 'ios' ? (
  47. <MapLibreRN.Marker
  48. id="selected_marker_callout"
  49. lngLat={marker.coordinates}
  50. anchor="bottom"
  51. >
  52. <Pressable onPressIn={() => { if (calloutPressRef) calloutPressRef.current = true; }}>
  53. <View style={styles.customView}>
  54. <View style={styles.calloutContainer}>
  55. <View style={styles.calloutImageContainer}>
  56. {!!marker.icon?.uri && (
  57. <Image
  58. source={{ uri: marker.icon.uri }}
  59. style={styles.calloutImage}
  60. resizeMode="contain"
  61. />
  62. )}
  63. </View>
  64. <View style={styles.calloutTextContainer}>
  65. <Text style={styles.seriesName}>{marker.series_name}</Text>
  66. <Text style={styles.markerName} selectable={true}>
  67. {marker.name}
  68. </Text>
  69. </View>
  70. <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
  71. <TouchableOpacity
  72. style={[
  73. styles.calloutButton,
  74. { flex: 1 },
  75. (marker.visited === 1 &&
  76. token && {
  77. backgroundColor: Colors.WHITE,
  78. borderWidth: 1,
  79. borderColor: Colors.BORDER_LIGHT
  80. }) ||
  81. {}
  82. ]}
  83. onPress={() => toggleSeries(marker)}
  84. >
  85. <View style={styles.completedContainer}>
  86. <View style={{ display: marker?.visited === 1 && token ? 'flex' : 'none' }}>
  87. <CheckSvg width={14} height={14} fill={Colors.DARK_BLUE} />
  88. </View>
  89. <Text
  90. style={[
  91. styles.calloutButtonText,
  92. marker?.visited === 1 && token ? { color: Colors.DARK_BLUE } : {}
  93. ]}
  94. >
  95. {marker?.visited === 1 && token ? 'Completed' : 'To Complete'}
  96. </Text>
  97. </View>
  98. </TouchableOpacity>
  99. <TouchableOpacity
  100. onPress={() => {
  101. Share.share(
  102. Platform.OS === 'ios'
  103. ? { url: `${API_HOST}/series-item/${marker.id}` }
  104. : { message: `${API_HOST}/series-item/${marker.id}` }
  105. );
  106. }}
  107. style={{ padding: 6, backgroundColor: Colors.FILL_LIGHT, borderRadius: 8, height: 30, marginBottom: 12 }}
  108. >
  109. <ShareIcon width={18} height={18} fill={Colors.DARK_BLUE} />
  110. </TouchableOpacity>
  111. </View>
  112. {parsedAvatars && (
  113. <TouchableOpacity
  114. onPress={() => {
  115. guardFeature(() => {
  116. navigation.navigate(
  117. ...([
  118. NAVIGATION_PAGES.USERS_LIST,
  119. {
  120. id: marker.id,
  121. isFromHere: false,
  122. type: 'series'
  123. }
  124. ] as never)
  125. );
  126. });
  127. }}
  128. style={styles.userImageContainer}
  129. >
  130. {parsedAvatars?.map((avatar: number, index: number) => (
  131. <Image
  132. key={`${avatar}-${index}`}
  133. source={{ uri: API_HOST + '/img/avatars/' + avatar + '.webp' }}
  134. style={styles.userImageSmall}
  135. />
  136. ))}
  137. <View style={styles.userCountContainer}>
  138. <Text style={styles.userCount}>{formattedCount}</Text>
  139. </View>
  140. </TouchableOpacity>
  141. )}
  142. </View>
  143. </View>
  144. </Pressable>
  145. </MapLibreRN.Marker>
  146. ) : (
  147. <MapLibreRN.Marker
  148. key={`${marker.id}-${marker.visited}`}
  149. id="selected_marker_callout"
  150. lngLat={marker.coordinates}
  151. anchor="bottom"
  152. >
  153. <Pressable onPressIn={() => { if (calloutPressRef) calloutPressRef.current = true; }}>
  154. <View style={styles.customView}>
  155. <View style={styles.calloutContainer}>
  156. <View style={styles.calloutImageContainer}>
  157. {!!marker.icon?.uri && (
  158. <Image
  159. source={{ uri: marker.icon.uri }}
  160. style={styles.calloutImage}
  161. resizeMode="contain"
  162. />
  163. )}
  164. </View>
  165. <View style={[styles.calloutTextContainer, { flex: 0 }]}>
  166. <Text style={styles.seriesName}>{marker.series_name}</Text>
  167. <Text style={styles.markerName} selectable={true}>
  168. {marker.name}
  169. </Text>
  170. </View>
  171. <View style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
  172. <TouchableOpacity
  173. style={[
  174. styles.calloutButton,
  175. { flex: 1 },
  176. (marker.visited === 1 &&
  177. token && {
  178. backgroundColor: Colors.WHITE,
  179. borderWidth: 1,
  180. borderColor: Colors.BORDER_LIGHT
  181. }) ||
  182. {}
  183. ]}
  184. onPressIn={() => toggleSeries(marker)}
  185. >
  186. <View style={styles.completedContainer}>
  187. <View style={{ display: marker?.visited === 1 && token ? 'flex' : 'none' }}>
  188. <CheckSvg width={14} height={14} fill={Colors.DARK_BLUE} />
  189. </View>
  190. <Text
  191. style={[
  192. styles.calloutButtonText,
  193. marker?.visited === 1 && token ? { color: Colors.DARK_BLUE } : {}
  194. ]}
  195. >
  196. {marker?.visited === 1 && token ? 'Completed' : 'To Complete'}
  197. </Text>
  198. </View>
  199. </TouchableOpacity>
  200. <TouchableOpacity
  201. onPressIn={() => {
  202. Share.share(
  203. Platform.OS === 'ios'
  204. ? { url: `${API_HOST}/series-item/${marker.id}` }
  205. : { message: `${API_HOST}/series-item/${marker.id}` }
  206. );
  207. }}
  208. style={{ padding: 6, backgroundColor: Colors.FILL_LIGHT, borderRadius: 8, height: 30, marginBottom: 12 }}
  209. >
  210. <ShareIcon width={18} height={18} fill={Colors.DARK_BLUE} />
  211. </TouchableOpacity>
  212. </View>
  213. {parsedAvatars && (
  214. <TouchableOpacity
  215. onPressIn={() => {
  216. guardFeature(() => {
  217. navigation.navigate(
  218. ...([
  219. NAVIGATION_PAGES.USERS_LIST,
  220. {
  221. id: marker.id,
  222. isFromHere: false,
  223. type: 'series'
  224. }
  225. ] as never)
  226. );
  227. });
  228. }}
  229. style={styles.userImageContainer}
  230. >
  231. {parsedAvatars?.map((avatar: number, index: number) => (
  232. <Image
  233. key={`${avatar}-${index}`}
  234. source={{ uri: API_HOST + '/img/avatars/' + avatar + '.webp' }}
  235. style={styles.userImageSmall}
  236. />
  237. ))}
  238. <View style={styles.userCountContainer}>
  239. <Text style={styles.userCount}>{formattedCount}</Text>
  240. </View>
  241. </TouchableOpacity>
  242. )}
  243. </View>
  244. </View>
  245. </Pressable>
  246. </MapLibreRN.Marker>
  247. )}
  248. </>
  249. );
  250. };
  251. export default MarkerItem;