tabView.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  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((item) => item.actual_total_power && item.actual_total_power !== 0)}
  88. renderItem={({ item }) => <TabItem item={item} currentLocation={currentLocation} />}
  89. estimatedItemSize={10}
  90. />
  91. )}
  92. </View>
  93. );
  94. const SecondRoute = React.useMemo(
  95. () =>
  96. React.memo(() => (
  97. <View style={{ flex: 1, backgroundColor: 'white' }}>
  98. <FlashList
  99. nestedScrollEnabled={true}
  100. data={completedReservationTabItems.filter(
  101. (item) => item.actual_total_power && item.actual_total_power > 1
  102. )}
  103. renderItem={({ item }) => <TabItem item={item} currentLocation={currentLocation} />}
  104. estimatedItemSize={20}
  105. />
  106. </View>
  107. )),
  108. [completedReservationTabItems]
  109. );
  110. // Use useCallback for renderScene
  111. // const renderScene = React.useCallback(
  112. // SceneMap({
  113. // firstRoute: FirstRoute,
  114. // secondRoute: SecondRoute
  115. // }),
  116. // [FirstRoute, SecondRoute]
  117. // );
  118. //uncallbacked it
  119. const renderScene = SceneMap({
  120. firstRoute: FirstRoute,
  121. secondRoute: SecondRoute
  122. });
  123. // const routes = React.useMemo(
  124. // () => [
  125. // { key: 'firstRoute', title: titles[0] },
  126. // { key: 'secondRoute', title: titles[1] }
  127. // ],
  128. // [titles]
  129. // );
  130. const routes = [
  131. { key: 'firstRoute', title: titles[0] },
  132. { key: 'secondRoute', title: titles[1] }
  133. ];
  134. const [index, setIndex] = React.useState(0);
  135. // const renderTabBar = React.useCallback(
  136. // (props: any) => (
  137. // <TabBar
  138. // {...props}
  139. // renderLabel={({ route, focused }) => (
  140. // <Text
  141. // style={{
  142. // color: focused ? '#025c72' : '#888888',
  143. // fontWeight: focused ? '900' : 'thin',
  144. // fontSize: 20
  145. // }}
  146. // >
  147. // {route.title}
  148. // </Text>
  149. // )}
  150. // indicatorStyle={{
  151. // backgroundColor: '#025c72'
  152. // }}
  153. // style={{
  154. // backgroundColor: 'white',
  155. // borderColor: '#DBE4E8',
  156. // elevation: 0,
  157. // marginHorizontal: 15,
  158. // borderBottomWidth: 0.5
  159. // }}
  160. // />
  161. // ),
  162. // []
  163. // );
  164. const renderTabBar = (props: any) => (
  165. <TabBar
  166. {...props}
  167. renderLabel={({ route, focused }) => (
  168. <Text
  169. style={{
  170. color: focused ? '#025c72' : '#888888',
  171. fontWeight: focused ? '900' : 'thin',
  172. fontSize: 20
  173. }}
  174. >
  175. {route.title}
  176. </Text>
  177. )}
  178. indicatorStyle={{
  179. backgroundColor: '#025c72'
  180. }}
  181. style={{
  182. backgroundColor: 'white',
  183. borderColor: '#DBE4E8',
  184. elevation: 0,
  185. marginHorizontal: 15,
  186. borderBottomWidth: 0.5
  187. }}
  188. />
  189. );
  190. return (
  191. <TabView
  192. navigationState={{ index, routes }}
  193. renderScene={renderScene}
  194. onIndexChange={setIndex}
  195. initialLayout={{ width: layout.width }}
  196. renderTabBar={renderTabBar}
  197. lazy
  198. />
  199. );
  200. };
  201. // Separate memoedTabItem component
  202. // const TabItem = React.memo(
  203. // ({ item, currentLocation }: { item: TabItem; currentLocation: Location.LocationObject | null }) => {
  204. // const [distance, setDistance] = useState<number | null>(null);
  205. // useEffect(() => {
  206. // const getDistance = async () => {
  207. // if (currentLocation) {
  208. // const result = await calculateDistance(
  209. // Number(item.stationLat),
  210. // Number(item.stationLng),
  211. // currentLocation
  212. // );
  213. // setDistance(result);
  214. // }
  215. // };
  216. // getDistance();
  217. // }, [currentLocation, item.stationLat, item.stationLng]);
  218. // const formatDistance = (distanceInMeters: number): string => {
  219. // if (distanceInMeters < 1000) {
  220. // return `${Math.round(distanceInMeters)}米`;
  221. // } else {
  222. // const distanceInKm = distanceInMeters / 1000;
  223. // return `${distanceInKm.toFixed(1)}公里`;
  224. // }
  225. // };
  226. // return (
  227. // <View style={styles.container}>
  228. // <Image style={styles.image} source={item.imgURL} />
  229. // <View style={styles.textContainer}>
  230. // <Text
  231. // style={{
  232. // fontWeight: '700',
  233. // color: '#02677D',
  234. // fontSize: 20
  235. // }}
  236. // >
  237. // {`${item.date} - ${item.time}`}
  238. // </Text>
  239. // <Text
  240. // style={{
  241. // fontWeight: '400',
  242. // fontSize: 16,
  243. // color: '#222222'
  244. // }}
  245. // >
  246. // {item.chargeStationName}
  247. // </Text>
  248. // <Text
  249. // style={{
  250. // fontWeight: '400',
  251. // fontSize: 16,
  252. // color: '#888888'
  253. // }}
  254. // >
  255. // {item.chargeStationAddress}
  256. // </Text>
  257. // </View>
  258. // <Text
  259. // style={{
  260. // fontWeight: '400',
  261. // fontSize: 16,
  262. // color: '#888888',
  263. // marginTop: 20,
  264. // marginLeft: 16
  265. // }}
  266. // >
  267. // {distance !== null ? formatDistance(distance) : ''}
  268. // </Text>
  269. // </View>
  270. // );
  271. // }
  272. // );
  273. const TabItem = ({ item, currentLocation }: { item: TabItem; currentLocation: Location.LocationObject | null }) => {
  274. const [distance, setDistance] = useState<number | null>(null);
  275. useEffect(() => {
  276. const getDistance = async () => {
  277. if (currentLocation) {
  278. const result = await calculateDistance(
  279. Number(item.stationLat),
  280. Number(item.stationLng),
  281. currentLocation
  282. );
  283. setDistance(result);
  284. }
  285. };
  286. getDistance();
  287. }, [currentLocation, item.stationLat, item.stationLng]);
  288. const formatDistance = (distanceInMeters: number): string => {
  289. if (distanceInMeters < 1000) {
  290. return `${Math.round(distanceInMeters)}米`;
  291. } else {
  292. const distanceInKm = distanceInMeters / 1000;
  293. return `${distanceInKm.toFixed(1)}公里`;
  294. }
  295. };
  296. return (
  297. <Pressable
  298. onPress={() => {
  299. console.log(item.format_order_id);
  300. }}
  301. >
  302. <View style={styles.container}>
  303. <Image style={styles.image} source={item.imgURL} />
  304. <View className="flex flex-col gap-2 mt-2 mr-2">
  305. <Text
  306. style={{
  307. fontWeight: '700',
  308. color: '#02677D',
  309. fontSize: 16
  310. }}
  311. >
  312. {`${item.date}日 - ${item.time}至${item.actual_end_time}`}
  313. </Text>
  314. <View className="flex flex-row justify-between space-x-2">
  315. <Text
  316. style={{
  317. fontWeight: '400',
  318. fontSize: 14,
  319. color: '#222222'
  320. }}
  321. >
  322. 已充電度數:{' '}
  323. {item.actual_total_power
  324. ? item.actual_total_power % 1 === 0
  325. ? item.actual_total_power
  326. : item.actual_total_power.toFixed(1)
  327. : ''}
  328. </Text>
  329. <Text
  330. style={{
  331. fontWeight: '400',
  332. fontSize: 14,
  333. color: '#222222'
  334. }}
  335. >
  336. 應付金額:{' '}
  337. {item.actual_fee !== undefined && item.actual_fee !== null
  338. ? item.actual_fee <= 0
  339. ? '$0'
  340. : item.actual_fee % 1 === 0
  341. ? `$${item.actual_fee}`
  342. : `$${item.actual_fee.toFixed(1)}`
  343. : ''}
  344. </Text>
  345. </View>
  346. <View className="flex flex-row space-x-2">
  347. <Text
  348. style={{
  349. fontWeight: '400',
  350. fontSize: 14,
  351. color: '#222222'
  352. }}
  353. >
  354. 每度電金額: ${item.current_price}
  355. </Text>
  356. </View>
  357. <Text
  358. style={{
  359. fontWeight: '400',
  360. fontSize: 14,
  361. color: '#888888'
  362. }}
  363. >
  364. {item.chargeStationName}
  365. </Text>
  366. </View>
  367. </View>
  368. </Pressable>
  369. );
  370. };
  371. export default TabViewComponent;
  372. const styles = StyleSheet.create({
  373. container: {
  374. flexDirection: 'row',
  375. width: '100%',
  376. flex: 1
  377. },
  378. image: {
  379. width: 100,
  380. height: 100,
  381. margin: 15,
  382. borderRadius: 10
  383. },
  384. textContainer: {
  385. flex: 1,
  386. flexDirection: 'column',
  387. gap: 8,
  388. marginTop: 20,
  389. marginRight: 8 // Add right margin to prevent text from touching the edge
  390. }
  391. });