reservationLocationPageComponent.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. import Svg, { Path, Rect } from 'react-native-svg';
  2. import * as React from 'react';
  3. import {
  4. View,
  5. Text,
  6. useWindowDimensions,
  7. StyleSheet,
  8. Image,
  9. ImageSourcePropType,
  10. Pressable,
  11. ActivityIndicator
  12. } from 'react-native';
  13. import { TabView, SceneMap, TabBar } from 'react-native-tab-view';
  14. import { FlashList } from '@shopify/flash-list';
  15. import { SafeAreaView } from 'react-native-safe-area-context';
  16. import { router } from 'expo-router';
  17. import { CheckMarkLogoSvg, CrossLogoSvg } from '../global/SVG';
  18. import { useEffect, useState } from 'react';
  19. import { chargeStationService } from '../../service/chargeStationService';
  20. import * as Location from 'expo-location';
  21. import { calculateDistance } from '../global/distanceCalculator';
  22. interface TabItem {
  23. imgURL: ImageSourcePropType;
  24. date: string;
  25. time: string;
  26. chargeStationName: string;
  27. chargeStationAddress: string;
  28. distance: string;
  29. stationID: string;
  30. lat: string;
  31. lng: string;
  32. }
  33. interface TabViewComponentProps {
  34. titles: string[];
  35. tabItems: TabItem[];
  36. }
  37. const ReservationLocationPage = () => {
  38. const [stations, setStations] = useState([]);
  39. const [TabItems, setTabItems] = useState<TabItem[]>([]);
  40. const [currentLocation, setCurrentLocation] = useState<Location.LocationObject | null>(null);
  41. const getCurrentLocation = async () => {
  42. let { status } = await Location.requestForegroundPermissionsAsync();
  43. if (status !== 'granted') {
  44. console.error('Permission to access location was denied');
  45. return;
  46. }
  47. let location = await Location.getLastKnownPositionAsync({});
  48. setCurrentLocation(location);
  49. };
  50. useEffect(() => {
  51. getCurrentLocation();
  52. }, []);
  53. const LocationTabComponent: React.FC<TabViewComponentProps> = ({ titles, tabItems }) => {
  54. const FirstRoute = () => (
  55. <View
  56. style={{
  57. flex: 1,
  58. backgroundColor: 'white'
  59. }}
  60. >
  61. {TabItems.length > 0 ? (
  62. <FlashList
  63. data={tabItems}
  64. renderItem={({ item }) => {
  65. return (
  66. <Pressable
  67. onPress={() =>
  68. router.push({
  69. pathname: '/resultDetailPage',
  70. params: {
  71. chargeStationAddress: item.chargeStationAddress,
  72. chargeStationName: item.chargeStationName,
  73. chargeStationID: item.stationID,
  74. chargeStationLng: item.lng,
  75. chargeStationLat: item.lat
  76. }
  77. })
  78. }
  79. style={({ pressed }) => [
  80. styles.container,
  81. {
  82. backgroundColor: pressed ? '#e7f2f8' : '#ffffff'
  83. }
  84. ]}
  85. >
  86. <View style={styles.container} className=" flex-1 flex-row ">
  87. <Image style={styles.image} source={item.imgURL} />
  88. <View style={styles.textContainer} className="flex-1 justify-evenly">
  89. <Text
  90. style={{
  91. fontWeight: 400,
  92. fontSize: 20,
  93. color: '#222222'
  94. }}
  95. >
  96. {item.chargeStationName}
  97. </Text>
  98. <Text
  99. style={{
  100. fontWeight: 400,
  101. fontSize: 14,
  102. color: '#888888'
  103. }}
  104. >
  105. {item.chargeStationAddress}
  106. </Text>
  107. <View className=" flex-row space-x-2 items-center">
  108. <CheckMarkLogoSvg />
  109. <Text
  110. style={{
  111. fontWeight: 400,
  112. fontSize: 14,
  113. color: '#222222'
  114. }}
  115. >
  116. Walk-in
  117. </Text>
  118. </View>
  119. </View>
  120. <Text
  121. style={{
  122. fontWeight: 400,
  123. fontSize: 16,
  124. color: '#888888',
  125. marginTop: 22,
  126. marginLeft: 'auto',
  127. marginRight: 10
  128. }}
  129. >
  130. {item.distance}
  131. </Text>
  132. </View>
  133. </Pressable>
  134. );
  135. }}
  136. estimatedItemSize={10}
  137. />
  138. ) : (
  139. <View className="flex-1 items-center justify-center">
  140. <ActivityIndicator size="large" />
  141. </View>
  142. )}
  143. </View>
  144. );
  145. //tab 2
  146. const SecondRoute = () => (
  147. <View
  148. style={{
  149. flex: 1,
  150. backgroundColor: 'white'
  151. }}
  152. >
  153. <FlashList
  154. data={tabItems}
  155. renderItem={({ item }) => {
  156. return (
  157. <Pressable
  158. onPress={() =>
  159. router.push({
  160. pathname: '/resultDetailPage',
  161. params: {
  162. chargeStationAddress: item.chargeStationAddress,
  163. chargeStationName: item.chargeStationName,
  164. chargeStationLng: item.lng,
  165. chargeStationLat: item.lat
  166. }
  167. })
  168. }
  169. style={({ pressed }) => [
  170. styles.container,
  171. {
  172. backgroundColor: pressed ? '#e7f2f8' : '#ffffff'
  173. }
  174. ]}
  175. >
  176. <View style={styles.container} className=" flex-1 flex-row ">
  177. <Image style={styles.image} source={item.imgURL} />
  178. <View style={styles.textContainer} className="flex-1 justify-evenly">
  179. <Text
  180. style={{
  181. fontWeight: 400,
  182. fontSize: 20,
  183. color: '#222222'
  184. }}
  185. >
  186. {item.chargeStationName}
  187. </Text>
  188. <Text
  189. style={{
  190. fontWeight: 400,
  191. fontSize: 14,
  192. color: '#888888'
  193. }}
  194. >
  195. {item.chargeStationAddress}
  196. </Text>
  197. <View className=" flex-row space-x-2 items-center">
  198. <CheckMarkLogoSvg />
  199. <Text
  200. style={{
  201. fontWeight: 400,
  202. fontSize: 14,
  203. color: '#222222'
  204. }}
  205. >
  206. Walk-in
  207. </Text>
  208. </View>
  209. </View>
  210. <Text
  211. style={{
  212. fontWeight: 400,
  213. fontSize: 16,
  214. color: '#888888',
  215. marginTop: 22,
  216. marginLeft: 'auto',
  217. marginRight: 10
  218. }}
  219. >
  220. {item.distance}
  221. </Text>
  222. </View>
  223. </Pressable>
  224. );
  225. }}
  226. estimatedItemSize={10}
  227. />
  228. </View>
  229. );
  230. const renderScene = SceneMap({
  231. firstRoute: FirstRoute,
  232. secondRoute: SecondRoute
  233. });
  234. const [routes] = React.useState([
  235. { key: 'firstRoute', title: titles[0] },
  236. { key: 'secondRoute', title: titles[1] }
  237. ]);
  238. const [index, setIndex] = React.useState(0);
  239. const renderTabBar = (props: any) => (
  240. <TabBar
  241. {...props}
  242. indicatorStyle={{
  243. backgroundColor: '#025c72'
  244. }}
  245. style={{
  246. backgroundColor: 'white',
  247. borderColor: '#DBE4E8',
  248. elevation: 0,
  249. borderBottomWidth: 0.5
  250. }}
  251. />
  252. );
  253. return (
  254. <TabView
  255. navigationState={{ index, routes }}
  256. renderScene={renderScene}
  257. onIndexChange={setIndex}
  258. // initialLayout={{ width: layout.width }}
  259. renderTabBar={renderTabBar}
  260. commonOptions={{
  261. label: ({ route, focused }) => (
  262. <Text
  263. style={{
  264. color: focused ? '#025c72' : '#888888',
  265. fontWeight: focused ? '900' : 'thin',
  266. fontSize: 20
  267. }}
  268. >
  269. {route.title}
  270. </Text>
  271. )
  272. }}
  273. />
  274. );
  275. };
  276. const styles = StyleSheet.create({
  277. container: { flexDirection: 'row' },
  278. image: {
  279. width: 100,
  280. height: 100,
  281. marginTop: 15,
  282. marginRight: 15,
  283. borderRadius: 10
  284. },
  285. textContainer: {
  286. flexDirection: 'column',
  287. gap: 4,
  288. marginTop: 15
  289. }
  290. });
  291. // const fetchStations = async () => {
  292. // const fetchedStations = await chargeStationService.fetchChargeStations();
  293. // setStations(fetchedStations);
  294. // const TabItems = fetchedStations.map((station) => ({
  295. // chargeStationAddress: station.Address,
  296. // chargeStationName: station.StationName,
  297. // lng: station.StationLng,
  298. // lat: station.StationLat,
  299. // date: '今天',
  300. // stationID: station.StationID,
  301. // imgURL: require('../../assets/dummyStationPicture.png'),
  302. // distance: '400米'
  303. // }));
  304. // setTabItems(TabItems);
  305. // };
  306. // fetchStations();
  307. // }, []);
  308. const fetchStations = async () => {
  309. const fetchedStations = await chargeStationService.fetchChargeStations();
  310. setStations(fetchedStations);
  311. if (currentLocation) {
  312. const TabItems = await Promise.all(
  313. fetchedStations.map(async (station) => {
  314. const distance = await calculateDistance(
  315. Number(station.StationLat),
  316. Number(station.StationLng),
  317. currentLocation
  318. );
  319. return {
  320. chargeStationAddress: station.Address,
  321. chargeStationName: station.StationName,
  322. lng: station.StationLng,
  323. lat: station.StationLat,
  324. date: '今天',
  325. stationID: station.StationID,
  326. imgURL: require('../../assets/dummyStationPicture.png'),
  327. distance: distance !== null ? formatDistance(distance) : 'N/A'
  328. };
  329. })
  330. );
  331. setTabItems(TabItems);
  332. }
  333. };
  334. useEffect(() => {
  335. if (currentLocation) {
  336. fetchStations();
  337. }
  338. }, [currentLocation]);
  339. const formatDistance = (distanceInMeters: number): string => {
  340. if (distanceInMeters < 1000) {
  341. return `${Math.round(distanceInMeters)}米`;
  342. } else {
  343. const distanceInKm = distanceInMeters / 1000;
  344. return `${distanceInKm.toFixed(1)}公里`;
  345. }
  346. };
  347. return (
  348. <SafeAreaView className="flex-1 bg-white" edges={['top', 'left', 'right']}>
  349. <View className="flex-1 mx-[5%] ">
  350. <View className="min-h-[250px] flex-column">
  351. <View className="flex-1 justify-center">
  352. <View className="pt-5 pl-4">
  353. <Pressable
  354. onPress={() => {
  355. if (router.canGoBack()) {
  356. router.back();
  357. } else {
  358. router.replace('/');
  359. }
  360. }}
  361. >
  362. <CrossLogoSvg />
  363. </Pressable>
  364. </View>
  365. </View>
  366. <View className="flex-1 justify-center ">
  367. <Text className="text-[50px] font-normal ">預約地點</Text>
  368. </View>
  369. <View className="flex-1 justify-center ">
  370. <Pressable onPress={() => router.push('searchPage')}>
  371. <View
  372. style={{
  373. borderWidth: 1,
  374. padding: 24,
  375. borderRadius: 12,
  376. borderColor: '#bbbbbb',
  377. maxWidth: '100%'
  378. }}
  379. >
  380. <Text
  381. style={{
  382. color: '#888888',
  383. fontSize: 16
  384. }}
  385. >
  386. 搜尋充電站或地區..
  387. </Text>
  388. </View>
  389. </Pressable>
  390. </View>
  391. </View>
  392. <View className="flex-1 mt-2">
  393. <LocationTabComponent titles={['附近的充電站', '所有的充電站']} tabItems={TabItems} />
  394. </View>
  395. </View>
  396. </SafeAreaView>
  397. );
  398. };
  399. export default ReservationLocationPage;