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}></Text>
  45. </ScrollView>
  46. );
  47. const renderScene = SceneMap({
  48. firstRoute: FirstRoute,
  49. secondRoute: SecondRoute
  50. });
  51. const [routes] = React.useState([
  52. { key: 'firstRoute', title: titles[0] },
  53. { key: 'secondRoute', title: titles[1] }
  54. ]);
  55. const [index, setIndex] = React.useState(0);
  56. const renderTabBar = (props: any) => (
  57. <TabBar
  58. {...props}
  59. renderLabel={({ route, focused }) => (
  60. <Text
  61. style={{
  62. color: focused ? '#000000' : '#CCCCCC',
  63. fontWeight: focused ? '300' : 'thin',
  64. fontSize: 17
  65. }}
  66. >
  67. {route.title}
  68. </Text>
  69. )}
  70. indicatorStyle={{
  71. backgroundColor: '#000000',
  72. height: 1
  73. }}
  74. style={{
  75. backgroundColor: 'white',
  76. elevation: 0,
  77. marginHorizontal: 15,
  78. borderBottomWidth: 0.5
  79. }}
  80. />
  81. );
  82. return (
  83. <TabView
  84. navigationState={{ index, routes }}
  85. renderScene={renderScene}
  86. onIndexChange={setIndex}
  87. initialLayout={{ width: layout.width }}
  88. renderTabBar={renderTabBar}
  89. />
  90. );
  91. };
  92. const ResultDetailPageComponent = () => {
  93. const params = useLocalSearchParams();
  94. const chargeStationID = params.chargeStationID as string;
  95. const chargeStationName = params.chargeStationName as string;
  96. const chargeStationAddress = params.chargeStationAddress as string;
  97. // const chargeStationLat = params.chargeStationLat as string;
  98. // const chargeStationLng = params.chargeStationLng as string;
  99. const [currentLocation, setCurrentLocation] = useState<Location.LocationObject | null>(null);
  100. const [distance, setDistance] = useState<string | null>(null);
  101. const [coordinates, setCoordinates] = useState<StationCoordinates | null>(null);
  102. const [price, setPrice] = useState('');
  103. useEffect(() => {
  104. const getCurrentLocation = async () => {
  105. let { status } = await Location.requestForegroundPermissionsAsync();
  106. if (status !== 'granted') {
  107. console.error('Permission to access location was denied');
  108. return;
  109. }
  110. let location = await Location.getLastKnownPositionAsync({});
  111. setCurrentLocation(location);
  112. };
  113. getCurrentLocation();
  114. }, []);
  115. useEffect(() => {
  116. const getDistance = async () => {
  117. if (currentLocation) {
  118. let stationLat, stationLng;
  119. if (params.chargeStationLat && params.chargeStationLng) {
  120. stationLat = Number(params.chargeStationLat);
  121. stationLng = Number(params.chargeStationLng);
  122. } else if (coordinates) {
  123. stationLat = coordinates.StationLat;
  124. stationLng = coordinates.StationLng;
  125. } else {
  126. console.log('No station coordinates available');
  127. return;
  128. }
  129. try {
  130. const distance = await calculateDistance(stationLat, stationLng, currentLocation);
  131. setDistance(formatDistance(distance));
  132. } catch (error) {
  133. console.error('Error calculating distance:', error);
  134. }
  135. }
  136. };
  137. getDistance();
  138. }, [params.chargeStationLat, params.chargeStationLng, coordinates, currentLocation]);
  139. useEffect(() => {
  140. const fetchPrice = async () => {
  141. try {
  142. const price = await chargeStationService.fetchChargeStationPrice(chargeStationID);
  143. setPrice(price);
  144. } catch (error) {
  145. console.error('Error fetching price:', error);
  146. }
  147. };
  148. fetchPrice();
  149. }, []);
  150. // console.log(chargeStationLat, chargeStationLng);
  151. const memoizedCoordinates = useMemo(() => {
  152. if (params.chargeStationLat && params.chargeStationLng) {
  153. return {
  154. StationLat: parseFloat(params.chargeStationLat as string),
  155. StationLng: parseFloat(params.chargeStationLng as string)
  156. };
  157. }
  158. return null;
  159. }, [params.chargeStationLat, params.chargeStationLng]);
  160. useEffect(() => {
  161. const fetchCoordinates = async () => {
  162. // If coordinates are provided in params, use them
  163. if (memoizedCoordinates) {
  164. setCoordinates(memoizedCoordinates);
  165. } else {
  166. // If not provided, fetch from API
  167. try {
  168. const response = await chargeStationService.fetchChargeStations();
  169. if (response && response.length > 0) {
  170. const station = response.find((s) => s.StationID === chargeStationID);
  171. if (station) {
  172. setCoordinates({
  173. StationLat: station.StationLat,
  174. StationLng: station.StationLng
  175. });
  176. }
  177. }
  178. } catch (error) {
  179. console.error('Error fetching coordinates:', error);
  180. }
  181. }
  182. };
  183. fetchCoordinates();
  184. }, [chargeStationID, memoizedCoordinates]);
  185. const formatDistance = (distanceInMeters: number): string => {
  186. if (distanceInMeters < 1000) {
  187. return `${Math.round(distanceInMeters)}米`;
  188. } else {
  189. const distanceInKm = distanceInMeters / 1000;
  190. return `${distanceInKm.toFixed(1)}公里`;
  191. }
  192. };
  193. const handleNavigationPress = () => {
  194. console.log('starting navigation press in resultDetail', coordinates);
  195. if (coordinates) {
  196. const { StationLat, StationLng } = coordinates;
  197. const label = encodeURIComponent(chargeStationName);
  198. const googleMapsUrl = `https://www.google.com/maps/search/?api=1&query=${StationLat},${StationLng}`;
  199. // Fallback URL for web browser
  200. const webUrl = `https://www.google.com/maps/dir/?api=1&destination=${StationLat},${StationLng}`;
  201. Linking.canOpenURL(googleMapsUrl)
  202. .then((supported) => {
  203. if (supported) {
  204. Linking.openURL(googleMapsUrl);
  205. } else {
  206. Linking.openURL(webUrl).catch((err) => {
  207. console.error('An error occurred', err);
  208. Alert.alert(
  209. 'Error',
  210. "Unable to open Google Maps. Please make sure it's installed on your device.",
  211. [{ text: 'OK' }],
  212. { cancelable: false }
  213. );
  214. });
  215. }
  216. })
  217. .catch((err) => console.error('An error occurred', err));
  218. }
  219. };
  220. const handleNavigaationPress = () => {
  221. const latitude = chargeStationLat;
  222. const longitude = chargeStationLng;
  223. console.log('latitude', latitude);
  224. console.log('longitude', longitude);
  225. const label = encodeURIComponent(chargeStationName);
  226. // Google Maps App URL
  227. const googleMapsUrl = `https://www.google.com/maps/search/?api=1&query=${latitude},${longitude}`;
  228. // Fallback URL for web browser
  229. const webUrl = `https://www.google.com/maps/dir/?api=1&destination=${latitude},${longitude}`;
  230. Linking.canOpenURL(googleMapsUrl)
  231. .then((supported) => {
  232. if (supported) {
  233. Linking.openURL(googleMapsUrl);
  234. } else {
  235. Linking.openURL(webUrl).catch((err) => {
  236. console.error('An error occurred', err);
  237. Alert.alert(
  238. 'Error',
  239. "Unable to open Google Maps. Please make sure it's installed on your device.",
  240. [{ text: 'OK' }],
  241. { cancelable: false }
  242. );
  243. });
  244. }
  245. })
  246. .catch((err) => console.error('An error occurred', err));
  247. };
  248. const handleAddBooking = () => {
  249. if (coordinates) {
  250. router.push({
  251. pathname: 'makingBookingPage',
  252. params: {
  253. chargeStationID: chargeStationID,
  254. chargeStationAddress: chargeStationAddress,
  255. chargeStationName: chargeStationName,
  256. chargeStationLat: coordinates.StationLat.toString(),
  257. chargeStationLng: coordinates.StationLng.toString()
  258. }
  259. });
  260. }
  261. };
  262. return (
  263. <SafeAreaView edges={['top', 'left', 'right']} className="flex-1 bg-white">
  264. <ScrollView className="flex-1 ">
  265. <View className="relative">
  266. <Image
  267. source={require('../../assets/dummyStationPicture.png')}
  268. resizeMode="cover"
  269. style={{ flex: 1, width: '100%' }}
  270. />
  271. <View className="absolute top-8 left-7 ">
  272. <Pressable
  273. onPress={() => {
  274. if (router.canGoBack()) {
  275. router.back();
  276. } else {
  277. router.replace('./');
  278. }
  279. }}
  280. >
  281. <PreviousPageSvg />
  282. </Pressable>
  283. </View>
  284. </View>
  285. <View className="flex-column mx-[5%] mt-[5%]">
  286. <View>
  287. <Text className="text-3xl">{chargeStationName}</Text>
  288. </View>
  289. <View className="flex-row justify-between items-center">
  290. <Text className="text-base" style={{ color: '#888888' }}>
  291. {chargeStationAddress}
  292. </Text>
  293. <NormalButton
  294. title={
  295. <View className="flex-row items-center justify-center text-center space-x-1">
  296. <DirectionLogoSvg />
  297. <Text className="text-base ">路線</Text>
  298. </View>
  299. }
  300. onPress={handleNavigationPress}
  301. extendedStyle={{
  302. backgroundColor: '#E3F2F8',
  303. borderRadius: 61,
  304. paddingHorizontal: 20,
  305. paddingVertical: 6
  306. }}
  307. />
  308. </View>
  309. <View className="flex-row space-x-2 items-center pb-4 ">
  310. <CheckMarkLogoSvg />
  311. <Text>Walk-In</Text>
  312. {/* <Text>{distance} </Text> */}
  313. </View>
  314. {coordinates ? (
  315. <NormalButton
  316. title={
  317. <View className="pr-2">
  318. <Text
  319. style={{
  320. color: '#FFFFFF',
  321. fontWeight: 700,
  322. fontSize: 20
  323. }}
  324. >
  325. + 新增預約
  326. </Text>
  327. </View>
  328. }
  329. // onPress={() => console.log('ab')}
  330. onPress={handleAddBooking}
  331. />
  332. ) : (
  333. <NormalButton
  334. title={
  335. <View className="pr-2">
  336. <Text
  337. style={{
  338. color: '#FFFFFF',
  339. fontWeight: 700,
  340. fontSize: 20
  341. }}
  342. >
  343. <ActivityIndicator />
  344. </Text>
  345. </View>
  346. }
  347. // onPress={() => console.log('ab')}
  348. onPress={handleAddBooking}
  349. />
  350. )}
  351. <View
  352. className="flex-1 flex-row min-h-[20px] border-slate-300 my-6 rounded-2xl"
  353. style={{ borderWidth: 1 }}
  354. >
  355. <View className="flex-1 m-4">
  356. <View className="flex-1 flex-row ">
  357. <View className=" flex-1 flex-column ustify-between">
  358. <Text className="text-xl " style={styles.text}>
  359. 收費
  360. </Text>
  361. <View className="flex-row items-center space-x-2">
  362. <Text className="text-3xl text-[#02677D]">${price}</Text>
  363. <Text style={styles.text}>每度電</Text>
  364. </View>
  365. </View>
  366. <View className="items-center justify-center">
  367. <View className="w-[1px] h-[60%] bg-[#CCCCCC]" />
  368. </View>
  369. <View className=" flex-1 pl-4 flex-column justify-between">
  370. <Text className="text-xl " style={styles.text}>
  371. 可用充電槍數目
  372. </Text>
  373. <View className="flex-row items-center space-x-2">
  374. <Text className="text-3xl text-[#02677D]">4</Text>
  375. </View>
  376. </View>
  377. </View>
  378. </View>
  379. </View>
  380. </View>
  381. <View className="min-h-[300px]">
  382. <Text className="text-xl pb-2 mx-[5%]" style={styles.text}>
  383. 充電站資訊
  384. </Text>
  385. <ChargingStationTabView titles={['充電插頭', '其他']} />
  386. </View>
  387. </ScrollView>
  388. </SafeAreaView>
  389. );
  390. };
  391. export default ResultDetailPageComponent;
  392. const styles = StyleSheet.create({
  393. text: {
  394. fontWeight: 300,
  395. color: '#000000'
  396. }
  397. });