index.tsx 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  1. import React, { useState } from 'react';
  2. import { View, Text, TouchableOpacity, Image } from 'react-native';
  3. import { useNavigation } from '@react-navigation/native';
  4. import moment from 'moment';
  5. import { TripsData } from '../../utils/types';
  6. import { API_HOST } from 'src/constants';
  7. import { Colors } from 'src/theme';
  8. import { NAVIGATION_PAGES } from 'src/types';
  9. import { styles } from './styles';
  10. import CalendarIcon from 'assets/icons/travels-screens/calendar.svg';
  11. import EditIcon from 'assets/icons/travels-screens/pen-to-square.svg';
  12. import ArrowIcon from 'assets/icons/chevron-left.svg';
  13. import WarningIcon from 'assets/icons/warning.svg';
  14. const formatDate = (dateString: string | null) => {
  15. if (!dateString) return;
  16. const date = moment(dateString);
  17. const formattedDate = date.format('MMM DD');
  18. const year = date.format('YYYY');
  19. return (
  20. <Text style={styles.alignCenter}>
  21. <Text style={styles.tripDateText}>{formattedDate}</Text>
  22. {'\n'}
  23. <Text style={styles.tripDateText}>{year}</Text>
  24. </Text>
  25. );
  26. };
  27. const getValidDate = (value?: string | null) => {
  28. if (!value) return null;
  29. const date = moment(value);
  30. return date.isValid() ? date : null;
  31. };
  32. const formatSingleLineDate = (dateString: string | null) => {
  33. const date = getValidDate(dateString);
  34. if (!date) return null;
  35. return `${date.format('MMM DD')} ${date.format('YYYY')}`;
  36. };
  37. const renderMissingDatesWarning = (withMarginBottom = true) => (
  38. <View
  39. style={{
  40. flexDirection: 'row',
  41. gap: 6,
  42. alignItems: 'center',
  43. marginBottom: withMarginBottom ? 10 : 0,
  44. flex: 1
  45. }}
  46. >
  47. <WarningIcon color={Colors.RED} width={16} height={16} />
  48. <Text
  49. style={{
  50. flex: 1,
  51. fontSize: 14,
  52. fontWeight: '600',
  53. color: Colors.RED,
  54. paddingRight: 4
  55. }}
  56. >
  57. Fill in exact dates to get accurate statistics.
  58. </Text>
  59. </View>
  60. );
  61. const getTripDateLabel = (item: TripsData) => {
  62. const validFrom = getValidDate(item.date_from);
  63. const validTo = getValidDate(item.date_to);
  64. if (!validFrom && !validTo) {
  65. if (item.year) {
  66. return <Text style={[styles.tripDateText, { marginLeft: 8 }]}>{item.year}</Text>;
  67. }
  68. return null;
  69. }
  70. const fromYear = validFrom ? validFrom.format('YYYY') : null;
  71. const toYear = validTo ? validTo.format('YYYY') : null;
  72. if (item.dates_missing === 1) {
  73. if (!fromYear && !toYear) return null;
  74. if (fromYear && toYear && fromYear === toYear) {
  75. return <Text style={[styles.tripDateText, { marginLeft: 8 }]}>{toYear}</Text>;
  76. }
  77. return (
  78. <>
  79. {fromYear ? <Text style={[styles.tripDateText, { marginLeft: 8 }]}>{fromYear}</Text> : null}
  80. {fromYear && toYear ? <Text style={styles.tripDateText}>-</Text> : null}
  81. {toYear ? <Text style={styles.tripDateText}>{toYear}</Text> : null}
  82. </>
  83. );
  84. }
  85. const hasValidFrom = !!validFrom;
  86. const hasValidTo = !!validTo;
  87. if (!hasValidFrom && !hasValidTo) return null;
  88. if (hasValidFrom && !hasValidTo) {
  89. return (
  90. <Text style={[styles.tripDateText, { marginLeft: 8 }]}>
  91. {formatSingleLineDate(item.date_from)}
  92. </Text>
  93. );
  94. }
  95. if (!hasValidFrom && hasValidTo) {
  96. return (
  97. <Text style={[styles.tripDateText, { marginLeft: 8 }]}>
  98. {formatSingleLineDate(item.date_to)}
  99. </Text>
  100. );
  101. }
  102. if (validFrom && validTo && item.date_from === item.date_to) {
  103. return (
  104. <Text style={[styles.tripDateText, { marginLeft: 8 }]}>
  105. {formatSingleLineDate(item.date_from)}
  106. </Text>
  107. );
  108. }
  109. return (
  110. <>
  111. <Text style={{ marginLeft: 8 }}>{formatDate(item.date_from)}</Text>
  112. <Text style={styles.tripDateText}>-</Text>
  113. {formatDate(item.date_to)}
  114. </>
  115. );
  116. };
  117. const TripItem = ({ item }: { item: TripsData }) => {
  118. const navigation = useNavigation();
  119. const [showAllRegions, setShowAllRegions] = useState(false);
  120. const MAX_VISIBLE_REGIONS = 3;
  121. const totalRegions = item.regions?.length || 0;
  122. const visibleRegions = showAllRegions
  123. ? item.regions
  124. : item.regions?.slice(0, MAX_VISIBLE_REGIONS);
  125. const validFrom = getValidDate(item.date_from);
  126. const validTo = getValidDate(item.date_to);
  127. const hasAnyValidDate = !!validFrom || !!validTo || !!item.year;;
  128. const shouldRenderWarningAbove = item.dates_missing === 1 && hasAnyValidDate;
  129. const shouldRenderWarningInsteadOfDate = item.dates_missing === 1 && !hasAnyValidDate;
  130. const tripDateLabel = getTripDateLabel(item);
  131. return (
  132. <View style={styles.tripItemContainer}>
  133. {shouldRenderWarningAbove ? renderMissingDatesWarning(true) : null}
  134. <View style={styles.tripHeaderContainer}>
  135. {shouldRenderWarningInsteadOfDate ? (
  136. renderMissingDatesWarning(false)
  137. ) : tripDateLabel ? (
  138. <View style={styles.tripDateContainer}>
  139. <CalendarIcon fill={Colors.DARK_BLUE} />
  140. {tripDateLabel}
  141. </View>
  142. ) : null}
  143. <TouchableOpacity
  144. style={styles.editButton}
  145. onPress={() =>
  146. navigation.navigate(
  147. ...([NAVIGATION_PAGES.ADD_TRIP_2025, { editTripId: item.id }] as never)
  148. )
  149. }
  150. >
  151. <EditIcon />
  152. <Text style={styles.editButtonText}>Edit</Text>
  153. </TouchableOpacity>
  154. </View>
  155. <View style={styles.divider} />
  156. {item.description && (
  157. <>
  158. <Text style={styles.descriptionTitle}>Description</Text>
  159. <Text style={styles.descriptionText}>{item.description}</Text>
  160. </>
  161. )}
  162. <Text style={styles.visitedRegionsTitle}>Visited regions</Text>
  163. <View style={styles.regionsContainer}>
  164. {visibleRegions?.map((region, index) => {
  165. if (!region.id || !region.region_name) return null;
  166. const [name, ...rest] = region.region_name?.split(/ – | - /);
  167. const subname = rest?.join(' - ');
  168. return (
  169. <View key={`${region.id}-${index}`} style={styles.regionItem}>
  170. <Image source={{ uri: API_HOST + region.flag1 }} style={styles.flagIcon} />
  171. {region.flag2 && (
  172. <Image
  173. source={{ uri: API_HOST + region.flag2 }}
  174. style={[styles.flagIcon, { marginLeft: -5 }]}
  175. />
  176. )}
  177. <View style={styles.nameContainer}>
  178. <Text style={styles.regionName}>{name}</Text>
  179. <Text style={styles.regionSubname}>{subname}</Text>
  180. </View>
  181. </View>
  182. );
  183. })}
  184. </View>
  185. {totalRegions > MAX_VISIBLE_REGIONS && (
  186. <TouchableOpacity
  187. style={styles.showMoreButton}
  188. hitSlop={{ top: 10, bottom: 10, left: 10, right: 10 }}
  189. onPress={() => setShowAllRegions(!showAllRegions)}
  190. >
  191. <ArrowIcon
  192. fill={Colors.DARK_BLUE}
  193. style={[styles.headerIcon, showAllRegions ? styles.rotate : null]}
  194. />
  195. <Text style={styles.showMoreText}>
  196. {showAllRegions ? 'Show less' : `Show more (${totalRegions - MAX_VISIBLE_REGIONS})`}
  197. </Text>
  198. </TouchableOpacity>
  199. )}
  200. </View>
  201. );
  202. };
  203. export default TripItem;