tabView.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. //the size of the TabView will follow its parent-container's size.
  2. import * as React from 'react';
  3. import * as Location from 'expo-location';
  4. import {
  5. View,
  6. Text,
  7. useWindowDimensions,
  8. StyleSheet,
  9. Image,
  10. ImageSourcePropType,
  11. ActivityIndicator,
  12. Pressable
  13. } from 'react-native';
  14. import { TabView, SceneMap, TabBar } from 'react-native-tab-view';
  15. import { FlashList } from '@shopify/flash-list';
  16. import { useEffect, useState } from 'react';
  17. import { calculateDistance } from './distanceCalculator';
  18. import { router } from 'expo-router';
  19. export interface TabItem {
  20. imgURL: ImageSourcePropType;
  21. date: string;
  22. time: string;
  23. chargeStationName: string;
  24. chargeStationAddress: string;
  25. stationLat: string | number;
  26. stationLng: string | number;
  27. distance: string;
  28. format_order_id: string;
  29. }
  30. interface TabViewComponentProps {
  31. titles: string[];
  32. tabItems: TabItem[];
  33. completedReservationTabItems: TabItem[];
  34. isLoading?: boolean;
  35. }
  36. const TabViewComponent: React.FC<TabViewComponentProps> = ({
  37. titles,
  38. isLoading,
  39. tabItems,
  40. completedReservationTabItems
  41. }) => {
  42. const layout = useWindowDimensions();
  43. const [currentLocation, setCurrentLocation] = useState<Location.LocationObject | null>(null);
  44. useEffect(() => {
  45. const getCurrentLocation = async () => {
  46. let { status } = await Location.requestForegroundPermissionsAsync();
  47. if (status !== 'granted') {
  48. console.error('Permission to access location was denied');
  49. return;
  50. }
  51. let location = await Location.getLastKnownPositionAsync({});
  52. setCurrentLocation(location);
  53. };
  54. getCurrentLocation();
  55. }, []);
  56. // Memoize the route components
  57. // const FirstRoute = React.useMemo(
  58. // () =>
  59. // React.memo(() => (
  60. // <View style={{ flex: 1, backgroundColor: 'white' }}>
  61. // {isLoading ? (
  62. // <View className="items-center justify-center flex-1">
  63. // <ActivityIndicator color="#34657b" />
  64. // </View>
  65. // ) : (
  66. // <FlashList
  67. // nestedScrollEnabled={true}
  68. // data={tabItems}
  69. // renderItem={({ item }) => <TabItem item={item} currentLocation={currentLocation} />}
  70. // estimatedItemSize={10}
  71. // />
  72. // )}
  73. // </View>
  74. // )),
  75. // [isLoading, tabItems]
  76. // );
  77. //i unmemoed it
  78. const FirstRoute = () => (
  79. <View style={{ flex: 1, backgroundColor: 'white' }}>
  80. {isLoading ? (
  81. <View className="items-center justify-center flex-1">
  82. <ActivityIndicator color="#34657b" />
  83. </View>
  84. ) : (
  85. <FlashList
  86. nestedScrollEnabled={true}
  87. data={tabItems.filter(
  88. (item) =>
  89. item.actual_fee &&
  90. item.actual_total_power &&
  91. item.actual_fee !== 0 &&
  92. item.actual_total_power !== 0
  93. )}
  94. renderItem={({ item }) => <TabItem item={item} currentLocation={currentLocation} />}
  95. estimatedItemSize={10}
  96. />
  97. )}
  98. </View>
  99. );
  100. const SecondRoute = React.useMemo(
  101. () =>
  102. React.memo(() => (
  103. <View style={{ flex: 1, backgroundColor: 'white' }}>
  104. <FlashList
  105. nestedScrollEnabled={true}
  106. data={completedReservationTabItems.filter(
  107. (item) =>
  108. item.actual_fee &&
  109. item.actual_total_power &&
  110. item.actual_fee > 1 &&
  111. item.actual_total_power > 1
  112. )}
  113. renderItem={({ item }) => <TabItem item={item} currentLocation={currentLocation} />}
  114. estimatedItemSize={10}
  115. />
  116. </View>
  117. )),
  118. [completedReservationTabItems]
  119. );
  120. // Use useCallback for renderScene
  121. // const renderScene = React.useCallback(
  122. // SceneMap({
  123. // firstRoute: FirstRoute,
  124. // secondRoute: SecondRoute
  125. // }),
  126. // [FirstRoute, SecondRoute]
  127. // );
  128. //uncallbacked it
  129. const renderScene = SceneMap({
  130. firstRoute: FirstRoute,
  131. secondRoute: SecondRoute
  132. });
  133. // const routes = React.useMemo(
  134. // () => [
  135. // { key: 'firstRoute', title: titles[0] },
  136. // { key: 'secondRoute', title: titles[1] }
  137. // ],
  138. // [titles]
  139. // );
  140. const routes = [
  141. { key: 'firstRoute', title: titles[0] },
  142. { key: 'secondRoute', title: titles[1] }
  143. ];
  144. const [index, setIndex] = React.useState(0);
  145. // const renderTabBar = React.useCallback(
  146. // (props: any) => (
  147. // <TabBar
  148. // {...props}
  149. // renderLabel={({ route, focused }) => (
  150. // <Text
  151. // style={{
  152. // color: focused ? '#025c72' : '#888888',
  153. // fontWeight: focused ? '900' : 'thin',
  154. // fontSize: 20
  155. // }}
  156. // >
  157. // {route.title}
  158. // </Text>
  159. // )}
  160. // indicatorStyle={{
  161. // backgroundColor: '#025c72'
  162. // }}
  163. // style={{
  164. // backgroundColor: 'white',
  165. // borderColor: '#DBE4E8',
  166. // elevation: 0,
  167. // marginHorizontal: 15,
  168. // borderBottomWidth: 0.5
  169. // }}
  170. // />
  171. // ),
  172. // []
  173. // );
  174. const renderTabBar = (props: any) => (
  175. <TabBar
  176. {...props}
  177. renderLabel={({ route, focused }) => (
  178. <Text
  179. style={{
  180. color: focused ? '#025c72' : '#888888',
  181. fontWeight: focused ? '900' : 'thin',
  182. fontSize: 20
  183. }}
  184. >
  185. {route.title}
  186. </Text>
  187. )}
  188. indicatorStyle={{
  189. backgroundColor: '#025c72'
  190. }}
  191. style={{
  192. backgroundColor: 'white',
  193. borderColor: '#DBE4E8',
  194. elevation: 0,
  195. marginHorizontal: 15,
  196. borderBottomWidth: 0.5
  197. }}
  198. />
  199. );
  200. return (
  201. <TabView
  202. navigationState={{ index, routes }}
  203. renderScene={renderScene}
  204. onIndexChange={setIndex}
  205. initialLayout={{ width: layout.width }}
  206. renderTabBar={renderTabBar}
  207. lazy
  208. />
  209. );
  210. };
  211. // Separate memoedTabItem component
  212. // const TabItem = React.memo(
  213. // ({ item, currentLocation }: { item: TabItem; currentLocation: Location.LocationObject | null }) => {
  214. // const [distance, setDistance] = useState<number | null>(null);
  215. // useEffect(() => {
  216. // const getDistance = async () => {
  217. // if (currentLocation) {
  218. // const result = await calculateDistance(
  219. // Number(item.stationLat),
  220. // Number(item.stationLng),
  221. // currentLocation
  222. // );
  223. // setDistance(result);
  224. // }
  225. // };
  226. // getDistance();
  227. // }, [currentLocation, item.stationLat, item.stationLng]);
  228. // const formatDistance = (distanceInMeters: number): string => {
  229. // if (distanceInMeters < 1000) {
  230. // return `${Math.round(distanceInMeters)}米`;
  231. // } else {
  232. // const distanceInKm = distanceInMeters / 1000;
  233. // return `${distanceInKm.toFixed(1)}公里`;
  234. // }
  235. // };
  236. // return (
  237. // <View style={styles.container}>
  238. // <Image style={styles.image} source={item.imgURL} />
  239. // <View style={styles.textContainer}>
  240. // <Text
  241. // style={{
  242. // fontWeight: '700',
  243. // color: '#02677D',
  244. // fontSize: 20
  245. // }}
  246. // >
  247. // {`${item.date} - ${item.time}`}
  248. // </Text>
  249. // <Text
  250. // style={{
  251. // fontWeight: '400',
  252. // fontSize: 16,
  253. // color: '#222222'
  254. // }}
  255. // >
  256. // {item.chargeStationName}
  257. // </Text>
  258. // <Text
  259. // style={{
  260. // fontWeight: '400',
  261. // fontSize: 16,
  262. // color: '#888888'
  263. // }}
  264. // >
  265. // {item.chargeStationAddress}
  266. // </Text>
  267. // </View>
  268. // <Text
  269. // style={{
  270. // fontWeight: '400',
  271. // fontSize: 16,
  272. // color: '#888888',
  273. // marginTop: 20,
  274. // marginLeft: 16
  275. // }}
  276. // >
  277. // {distance !== null ? formatDistance(distance) : ''}
  278. // </Text>
  279. // </View>
  280. // );
  281. // }
  282. // );
  283. const TabItem = ({ item, currentLocation }: { item: TabItem; currentLocation: Location.LocationObject | null }) => {
  284. const [distance, setDistance] = useState<number | null>(null);
  285. useEffect(() => {
  286. const getDistance = async () => {
  287. if (currentLocation) {
  288. const result = await calculateDistance(
  289. Number(item.stationLat),
  290. Number(item.stationLng),
  291. currentLocation
  292. );
  293. setDistance(result);
  294. }
  295. };
  296. getDistance();
  297. }, [currentLocation, item.stationLat, item.stationLng]);
  298. const formatDistance = (distanceInMeters: number): string => {
  299. if (distanceInMeters < 1000) {
  300. return `${Math.round(distanceInMeters)}米`;
  301. } else {
  302. const distanceInKm = distanceInMeters / 1000;
  303. return `${distanceInKm.toFixed(1)}公里`;
  304. }
  305. };
  306. return (
  307. <Pressable
  308. onPress={() => {
  309. console.log(item.format_order_id);
  310. }}
  311. >
  312. <View style={styles.container}>
  313. <Image style={styles.image} source={item.imgURL} />
  314. <View className="flex flex-col gap-2 mt-2 mr-2">
  315. <Text
  316. style={{
  317. fontWeight: '700',
  318. color: '#02677D',
  319. fontSize: 16
  320. }}
  321. >
  322. {`${item.date}日 - ${item.time}至${item.actual_end_time}`}
  323. </Text>
  324. <View className="flex flex-row justify-between space-x-2">
  325. <Text
  326. style={{
  327. fontWeight: '400',
  328. fontSize: 14,
  329. color: '#222222'
  330. }}
  331. >
  332. 已充電度數:{' '}
  333. {item.actual_total_power
  334. ? item.actual_total_power % 1 === 0
  335. ? item.actual_total_power
  336. : item.actual_total_power.toFixed(1)
  337. : ''}
  338. </Text>
  339. <Text
  340. style={{
  341. fontWeight: '400',
  342. fontSize: 14,
  343. color: '#222222'
  344. }}
  345. >
  346. 已付金額:{' '}
  347. {item.actual_fee
  348. ? item.actual_fee % 1 === 0
  349. ? `$${item.actual_fee}`
  350. : `$${item.actual_fee.toFixed(1)}`
  351. : ''}
  352. </Text>
  353. </View>
  354. <View className="flex flex-row space-x-2">
  355. <Text
  356. style={{
  357. fontWeight: '400',
  358. fontSize: 14,
  359. color: '#222222'
  360. }}
  361. >
  362. 每度電金額: ${item.current_price}
  363. </Text>
  364. </View>
  365. <Text
  366. style={{
  367. fontWeight: '400',
  368. fontSize: 14,
  369. color: '#888888'
  370. }}
  371. >
  372. {item.chargeStationName}
  373. </Text>
  374. </View>
  375. </View>
  376. </Pressable>
  377. );
  378. };
  379. export default TabViewComponent;
  380. const styles = StyleSheet.create({
  381. container: {
  382. flexDirection: 'row',
  383. width: '100%',
  384. flex: 1
  385. },
  386. image: {
  387. width: 100,
  388. height: 100,
  389. margin: 15,
  390. borderRadius: 10
  391. },
  392. textContainer: {
  393. flex: 1,
  394. flexDirection: 'column',
  395. gap: 8,
  396. marginTop: 20,
  397. marginRight: 8 // Add right margin to prevent text from touching the edge
  398. }
  399. });