resultDetailPageComponent.tsx 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. import {
  2. View,
  3. Text,
  4. ScrollView,
  5. Image,
  6. useWindowDimensions,
  7. StyleSheet,
  8. Pressable,
  9. Platform,
  10. Linking,
  11. ActivityIndicator,
  12. Alert
  13. } from 'react-native';
  14. import React, { useEffect, useMemo, useState } from 'react';
  15. import { SceneMap, TabBar, TabView } from 'react-native-tab-view';
  16. import NormalButton from '../global/normal_button';
  17. import { router, useLocalSearchParams } from 'expo-router';
  18. import { CheckMarkLogoSvg, DirectionLogoSvg, PreviousPageSvg } from '../global/SVG';
  19. import { SafeAreaView } from 'react-native-safe-area-context';
  20. import { chargeStationService } from '../../service/chargeStationService';
  21. import * as Location from 'expo-location';
  22. import { calculateDistance } from '../global/distanceCalculator';
  23. interface ChargingStationTabViewProps {
  24. titles: string[];
  25. }
  26. interface StationCoordinates {
  27. StationLat: number;
  28. StationLng: number;
  29. }
  30. const ChargingStationTabView: React.FC<ChargingStationTabViewProps> = ({ titles }) => {
  31. const layout = useWindowDimensions();
  32. //tab 1
  33. const FirstRoute = () => (
  34. <ScrollView style={{ flex: 1, marginHorizontal: '5%' }}>
  35. <Text className="text-lg" style={styles.text}>
  36. 由於充電站車流眾多, 敬請客戶務必於預約時間的十五分鐘內到達充電站。
  37. 若客戶逾時超過15分鐘,系統將視作自動放棄預約,客戶需要重新預約一次。 本公司有權保留全數費用,恕不退還。
  38. </Text>
  39. </ScrollView>
  40. );
  41. //tab 2
  42. const SecondRoute = () => (
  43. <ScrollView style={{ flex: 1, marginHorizontal: '5%' }}>
  44. <Text className="text-lg " style={styles.text}>
  45. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et
  46. dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip
  47. ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu
  48. fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident
  49. </Text>
  50. </ScrollView>
  51. );
  52. const renderScene = SceneMap({
  53. firstRoute: FirstRoute,
  54. secondRoute: SecondRoute
  55. });
  56. const [routes] = React.useState([
  57. { key: 'firstRoute', title: titles[0] },
  58. { key: 'secondRoute', title: titles[1] }
  59. ]);
  60. const [index, setIndex] = React.useState(0);
  61. const renderTabBar = (props: any) => (
  62. <TabBar
  63. {...props}
  64. renderLabel={({ route, focused }) => (
  65. <Text
  66. style={{
  67. color: focused ? '#000000' : '#CCCCCC',
  68. fontWeight: focused ? '300' : 'thin',
  69. fontSize: 17
  70. }}
  71. >
  72. {route.title}
  73. </Text>
  74. )}
  75. indicatorStyle={{
  76. backgroundColor: '#000000',
  77. height: 1
  78. }}
  79. style={{
  80. backgroundColor: 'white',
  81. elevation: 0,
  82. marginHorizontal: 15,
  83. borderBottomWidth: 0.5
  84. }}
  85. />
  86. );
  87. return (
  88. <TabView
  89. navigationState={{ index, routes }}
  90. renderScene={renderScene}
  91. onIndexChange={setIndex}
  92. initialLayout={{ width: layout.width }}
  93. renderTabBar={renderTabBar}
  94. />
  95. );
  96. };
  97. const ResultDetailPageComponent = () => {
  98. const params = useLocalSearchParams();
  99. const chargeStationID = params.chargeStationID as string;
  100. const chargeStationName = params.chargeStationName as string;
  101. const chargeStationAddress = params.chargeStationAddress as string;
  102. // const chargeStationLat = params.chargeStationLat as string;
  103. // const chargeStationLng = params.chargeStationLng as string;
  104. const [currentLocation, setCurrentLocation] = useState<Location.LocationObject | null>(null);
  105. const [distance, setDistance] = useState<string | null>(null);
  106. const [coordinates, setCoordinates] = useState<StationCoordinates | null>(null);
  107. const [price, setPrice] = useState('');
  108. useEffect(() => {
  109. const getCurrentLocation = async () => {
  110. let { status } = await Location.requestForegroundPermissionsAsync();
  111. if (status !== 'granted') {
  112. console.error('Permission to access location was denied');
  113. return;
  114. }
  115. let location = await Location.getLastKnownPositionAsync({});
  116. setCurrentLocation(location);
  117. };
  118. getCurrentLocation();
  119. }, []);
  120. useEffect(() => {
  121. const getDistance = async () => {
  122. if (currentLocation) {
  123. let stationLat, stationLng;
  124. if (params.chargeStationLat && params.chargeStationLng) {
  125. stationLat = Number(params.chargeStationLat);
  126. stationLng = Number(params.chargeStationLng);
  127. } else if (coordinates) {
  128. stationLat = coordinates.StationLat;
  129. stationLng = coordinates.StationLng;
  130. } else {
  131. console.log('No station coordinates available');
  132. return;
  133. }
  134. try {
  135. const distance = await calculateDistance(stationLat, stationLng, currentLocation);
  136. setDistance(formatDistance(distance));
  137. } catch (error) {
  138. console.error('Error calculating distance:', error);
  139. }
  140. }
  141. };
  142. getDistance();
  143. }, [params.chargeStationLat, params.chargeStationLng, coordinates, currentLocation]);
  144. useEffect(() => {
  145. const fetchPrice = async () => {
  146. try {
  147. const price = await chargeStationService.fetchChargeStationPrice(chargeStationID);
  148. setPrice(price);
  149. } catch (error) {
  150. console.error('Error fetching price:', error);
  151. }
  152. };
  153. fetchPrice();
  154. }, []);
  155. // console.log(chargeStationLat, chargeStationLng);
  156. const memoizedCoordinates = useMemo(() => {
  157. if (params.chargeStationLat && params.chargeStationLng) {
  158. return {
  159. StationLat: parseFloat(params.chargeStationLat as string),
  160. StationLng: parseFloat(params.chargeStationLng as string)
  161. };
  162. }
  163. return null;
  164. }, [params.chargeStationLat, params.chargeStationLng]);
  165. useEffect(() => {
  166. const fetchCoordinates = async () => {
  167. // If coordinates are provided in params, use them
  168. if (memoizedCoordinates) {
  169. setCoordinates(memoizedCoordinates);
  170. } else {
  171. // If not provided, fetch from API
  172. try {
  173. const response = await chargeStationService.fetchChargeStations();
  174. if (response && response.length > 0) {
  175. const station = response.find((s) => s.StationID === chargeStationID);
  176. if (station) {
  177. setCoordinates({
  178. StationLat: station.StationLat,
  179. StationLng: station.StationLng
  180. });
  181. }
  182. }
  183. } catch (error) {
  184. console.error('Error fetching coordinates:', error);
  185. }
  186. }
  187. };
  188. fetchCoordinates();
  189. }, [chargeStationID, memoizedCoordinates]);
  190. const formatDistance = (distanceInMeters: number): string => {
  191. if (distanceInMeters < 1000) {
  192. return `${Math.round(distanceInMeters)}米`;
  193. } else {
  194. const distanceInKm = distanceInMeters / 1000;
  195. return `${distanceInKm.toFixed(1)}公里`;
  196. }
  197. };
  198. const handleNavigationPress = () => {
  199. if (coordinates) {
  200. const { StationLat, StationLng } = coordinates;
  201. const googleMapsUrl = `google.navigation:q=${StationLat},${StationLng}`;
  202. const webUrl = `https://www.google.com/maps/dir/?api=1&destination=${StationLat},${StationLng}`;
  203. Linking.canOpenURL(googleMapsUrl)
  204. .then((supported) => {
  205. if (supported) {
  206. Linking.openURL(googleMapsUrl);
  207. } else {
  208. Linking.openURL(webUrl).catch((err) => {
  209. console.error('An error occurred', err);
  210. Alert.alert(
  211. 'Error',
  212. "Unable to open Google Maps. Please make sure it's installed on your device.",
  213. [{ text: 'OK' }],
  214. { cancelable: false }
  215. );
  216. });
  217. }
  218. })
  219. .catch((err) => console.error('An error occurred', err));
  220. }
  221. };
  222. const handleNavigaationPress = () => {
  223. const latitude = chargeStationLat;
  224. const longitude = chargeStationLng;
  225. console.log('latitude', latitude);
  226. console.log('longitude', longitude);
  227. const label = encodeURIComponent(chargeStationName);
  228. // Google Maps App URL
  229. const googleMapsUrl = `https://www.google.com/maps/search/?api=1&query=${latitude},${longitude}`;
  230. // Fallback URL for web browser
  231. const webUrl = `https://www.google.com/maps/dir/?api=1&destination=${latitude},${longitude}`;
  232. Linking.canOpenURL(googleMapsUrl)
  233. .then((supported) => {
  234. if (supported) {
  235. Linking.openURL(googleMapsUrl);
  236. } else {
  237. Linking.openURL(webUrl).catch((err) => {
  238. console.error('An error occurred', err);
  239. Alert.alert(
  240. 'Error',
  241. "Unable to open Google Maps. Please make sure it's installed on your device.",
  242. [{ text: 'OK' }],
  243. { cancelable: false }
  244. );
  245. });
  246. }
  247. })
  248. .catch((err) => console.error('An error occurred', err));
  249. };
  250. const handleAddBooking = () => {
  251. if (coordinates) {
  252. router.push({
  253. pathname: 'makingBookingPage',
  254. params: {
  255. chargeStationID: chargeStationID,
  256. chargeStationAddress: chargeStationAddress,
  257. chargeStationName: chargeStationName,
  258. chargeStationLat: coordinates.StationLat.toString(),
  259. chargeStationLng: coordinates.StationLng.toString()
  260. }
  261. });
  262. }
  263. };
  264. return (
  265. <SafeAreaView edges={['top', 'left', 'right']} className="flex-1 bg-white">
  266. <ScrollView className="flex-1 ">
  267. <View className="relative">
  268. <Image
  269. source={require('../../assets/dummyStationPicture.png')}
  270. resizeMode="cover"
  271. style={{ flex: 1, width: '100%' }}
  272. />
  273. <View className="absolute top-8 left-7 ">
  274. <Pressable
  275. onPress={() => {
  276. if (router.canGoBack()) {
  277. router.back();
  278. } else {
  279. router.replace('./');
  280. }
  281. }}
  282. >
  283. <PreviousPageSvg />
  284. </Pressable>
  285. </View>
  286. </View>
  287. <View className="flex-column mx-[5%] mt-[5%]">
  288. <View>
  289. <Text className="text-3xl">{chargeStationName}</Text>
  290. </View>
  291. <View className="flex-row justify-between items-center">
  292. <Text className="text-base" style={{ color: '#888888' }}>
  293. {chargeStationAddress}
  294. </Text>
  295. <NormalButton
  296. title={
  297. <View className="flex-row items-center justify-center text-center space-x-1">
  298. <DirectionLogoSvg />
  299. <Text className="text-base ">路線</Text>
  300. </View>
  301. }
  302. onPress={handleNavigationPress}
  303. extendedStyle={{
  304. backgroundColor: '#E3F2F8',
  305. borderRadius: 61,
  306. paddingHorizontal: 20,
  307. paddingVertical: 6
  308. }}
  309. />
  310. </View>
  311. <View className="flex-row space-x-2 items-center pb-4 ">
  312. <CheckMarkLogoSvg />
  313. <Text>Walk-In</Text>
  314. {/* <Text>{distance} </Text> */}
  315. </View>
  316. {coordinates ? (
  317. <NormalButton
  318. title={
  319. <View className="pr-2">
  320. <Text
  321. style={{
  322. color: '#FFFFFF',
  323. fontWeight: 700,
  324. fontSize: 20
  325. }}
  326. >
  327. + 新增預約
  328. </Text>
  329. </View>
  330. }
  331. // onPress={() => console.log('ab')}
  332. onPress={handleAddBooking}
  333. />
  334. ) : (
  335. <NormalButton
  336. title={
  337. <View className="pr-2">
  338. <Text
  339. style={{
  340. color: '#FFFFFF',
  341. fontWeight: 700,
  342. fontSize: 20
  343. }}
  344. >
  345. <ActivityIndicator />
  346. </Text>
  347. </View>
  348. }
  349. // onPress={() => console.log('ab')}
  350. onPress={handleAddBooking}
  351. />
  352. )}
  353. <View
  354. className="flex-1 flex-row min-h-[20px] border-slate-300 my-6 rounded-2xl"
  355. style={{ borderWidth: 1 }}
  356. >
  357. <View className="flex-1 m-4">
  358. <View className="flex-1 flex-row ">
  359. <View className=" flex-1 flex-column justify-between">
  360. <Text className="text-xl " style={styles.text}>
  361. 收費
  362. </Text>
  363. <View className="flex-row items-center space-x-2">
  364. <Text className="text-3xl text-[#02677D]">$20</Text>
  365. <Text style={styles.text}>每15分鐘</Text>
  366. </View>
  367. </View>
  368. <View className="items-center justify-center">
  369. <View className="w-[1px] h-[60%] bg-[#CCCCCC]" />
  370. </View>
  371. <View className="flex-1 flex-column ">
  372. <View className="flex-1"></View>
  373. <View className="flex-row items-center ml-4 space-x-2 ">
  374. <Text className="text-3xl text-[#02677D]">${price}</Text>
  375. <Text style={styles.text}>每度電</Text>
  376. </View>
  377. </View>
  378. </View>
  379. </View>
  380. </View>
  381. </View>
  382. <View className="min-h-[300px]">
  383. <Text className="text-xl pb-2 mx-[5%]" style={styles.text}>
  384. 充電站資訊
  385. </Text>
  386. <ChargingStationTabView titles={['充電插頭', '其他']} />
  387. </View>
  388. </ScrollView>
  389. </SafeAreaView>
  390. );
  391. };
  392. export default ResultDetailPageComponent;
  393. const styles = StyleSheet.create({
  394. text: {
  395. fontWeight: 300,
  396. color: '#000000'
  397. }
  398. });