resultDetailPageComponent.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  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, { useCallback, 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, useFocusEffect, 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. import { ChargingDetails, Remark, PriceWeek, Special } from '../../service/type/chargeStationType';
  24. interface ChargingStationTabViewProps {
  25. titles: string[];
  26. pricemodel_id: string;
  27. }
  28. interface ChargingPeriod{
  29. event_name: string;
  30. price: number;
  31. from: string;
  32. to: string;
  33. }
  34. const ChargingStationTabView: React.FC<ChargingStationTabViewProps> = ({ titles, pricemodel_id }) => {
  35. const layout = useWindowDimensions();
  36. const [list, setList] = useState<ChargingPeriod []>([])
  37. const [strWeek, setStrWeek] = useState<string>('')
  38. useEffect(() => {
  39. chargeStationService.fetchElectricityPrice(pricemodel_id || 'a').then(res => {
  40. const date = new Date();
  41. const str = (date.toLocaleString('en-US', { weekday: 'short' })).toLowerCase();
  42. setStrWeek(date.toLocaleString('zh', { weekday: 'long' }))
  43. const newList = [] as ChargingPeriod[]
  44. res?.forEach((item) => {
  45. const obj = item[str as keyof PriceWeek]
  46. newList.push({event_name: item.event_name, price: obj.price, from: obj.from, to: obj.to})
  47. setList(newList)
  48. })
  49. })
  50. }, [pricemodel_id])
  51. //tab 1
  52. const FirstRoute = () => (
  53. <ScrollView style={{ flex: 1, marginHorizontal: '5%' }}>
  54. <View>
  55. <View className='w-full flex-row justify-between mt-4 pr-10'>
  56. <Text style={styles.leftLable}>時段</Text>
  57. <Text style={styles.rightLable}>價格(/度)</Text>
  58. </View>
  59. {
  60. list.map((item, index) => (
  61. <View key={index} className='w-full flex-row justify-between mt-3 pr-14'>
  62. <Text style={styles.leftLable}>{item.event_name}({item.from}-{item.to}):</Text>
  63. <Text style={styles.rightLable}>${item.price}</Text>
  64. </View>
  65. ))
  66. }
  67. </View>
  68. </ScrollView>
  69. );
  70. //tab 2
  71. const SecondRoute = () => (
  72. <ScrollView style={{ flex: 1, marginHorizontal: '5%' }}>
  73. <Text className="text-lg " style={styles.text}></Text>
  74. </ScrollView>
  75. );
  76. const renderScene = SceneMap({
  77. firstRoute: FirstRoute,
  78. secondRoute: SecondRoute
  79. });
  80. const [routes] = React.useState([
  81. { key: 'firstRoute', title: titles[0] },
  82. { key: 'secondRoute', title: titles[1] }
  83. ]);
  84. const [index, setIndex] = React.useState(0);
  85. const renderTabBar = (props: any) => (
  86. <TabBar
  87. {...props}
  88. indicatorStyle={{
  89. backgroundColor: '#000000',
  90. height: 1
  91. }}
  92. style={{
  93. backgroundColor: 'white',
  94. elevation: 0,
  95. marginHorizontal: 15,
  96. borderBottomWidth: 0.5
  97. }}
  98. />
  99. );
  100. return (
  101. <TabView
  102. navigationState={{ index, routes }}
  103. renderScene={renderScene}
  104. onIndexChange={setIndex}
  105. initialLayout={{ width: layout.width }}
  106. renderTabBar={renderTabBar}
  107. commonOptions={{
  108. label: ({ route, focused }) => (
  109. <Text
  110. style={{
  111. color: focused ? '#000000' : '#CCCCCC',
  112. fontWeight: focused ? '300' : 'thin',
  113. fontSize: 17
  114. }}
  115. >
  116. {route.title}
  117. </Text>
  118. )
  119. }}
  120. />
  121. );
  122. };
  123. const ResultDetailPageComponent = () => {
  124. const params = useLocalSearchParams();
  125. const chargeStationID = params.chargeStationID as string;
  126. const chargeStationName = params.chargeStationName as string;
  127. const chargeStationAddress = params.chargeStationAddress as string;
  128. const pricemodel_id = params.pricemodel_id as string;
  129. const imageSourceProps = params.imageSource;
  130. const stationLng = params.stationLng as string;
  131. const stationLat = params.stationLat as string;
  132. const [isLoading, setIsLoading] = useState(true);
  133. const [imageSource, setImageSource] = useState();
  134. const [currentLocation, setCurrentLocation] = useState<Location.LocationObject | null>(null);
  135. const [price, setPrice] = useState('');
  136. const [newAvailableConnectors, setNewAvailableConnectors] = useState<any>([]);
  137. useEffect(() => {
  138. const imgObj = imageSourceProps? {uri: imageSourceProps} : require('../../assets/dummyStationPicture.png')
  139. setImageSource(imgObj);
  140. }, [imageSourceProps])
  141. useEffect(() => {
  142. const fetchPrice = async () => {
  143. try {
  144. const price = await chargeStationService.fetchChargeStationPrice(chargeStationID);
  145. setPrice(price);
  146. } catch (error) {
  147. console.error('Error fetching price:', error);
  148. }
  149. };
  150. const getCurrentLocation = async () => {
  151. let { status } = await Location.requestForegroundPermissionsAsync();
  152. if (status !== 'granted') {
  153. console.error('Permission to access location was denied');
  154. return;
  155. }
  156. let location = await Location.getLastKnownPositionAsync({});
  157. setCurrentLocation(location);
  158. };
  159. getCurrentLocation();
  160. fetchPrice();
  161. }, []);
  162. useFocusEffect(
  163. useCallback(() => {
  164. setIsLoading(true);
  165. let isMounted = true; // Simple cleanup flag
  166. const fetchAllConnectors = async () => {
  167. try {
  168. const newAvailableConnectors = await chargeStationService.NewfetchAvailableConnectors();
  169. // Only update state if component is still mounted
  170. if (isMounted) {
  171. setNewAvailableConnectors(newAvailableConnectors);
  172. }
  173. } catch (error) {
  174. console.error('Fetch error:', error);
  175. }
  176. };
  177. fetchAllConnectors();
  178. setIsLoading(false);
  179. // Simple cleanup - prevents state updates if component unmounts
  180. return () => {
  181. isMounted = false;
  182. };
  183. }, []) // Add any missing dependencies here if needed
  184. );
  185. const handleNavigationPress = (StationLat: string, StationLng: string) => {
  186. console.log('starting navigation press in resultDetail', StationLat, StationLng);
  187. if (StationLat && StationLng) {
  188. const label = encodeURIComponent(chargeStationName);
  189. const googleMapsUrl = `https://www.google.com/maps/search/?api=1&query=${StationLat},${StationLng}`;
  190. // Fallback URL for web browser
  191. const webUrl = `https://www.google.com/maps/dir/?api=1&destination=${StationLat},${StationLng}`;
  192. Linking.canOpenURL(googleMapsUrl)
  193. .then((supported) => {
  194. if (supported) {
  195. Linking.openURL(googleMapsUrl);
  196. } else {
  197. Linking.openURL(webUrl).catch((err) => {
  198. console.error('An error occurred', err);
  199. Alert.alert(
  200. 'Error',
  201. "Unable to open Google Maps. Please make sure it's installed on your device.",
  202. [{ text: 'OK' }],
  203. { cancelable: false }
  204. );
  205. });
  206. }
  207. })
  208. .catch((err) => console.error('An error occurred', err));
  209. }
  210. };
  211. return (
  212. <SafeAreaView edges={['top', 'left', 'right']} className="flex-1 bg-white">
  213. <ScrollView className="flex-1 ">
  214. <View className="relative">
  215. <Image
  216. source={ imageSource }
  217. resizeMode="cover"
  218. style={{ flex: 1, width: '100%', height: 300 }}
  219. />
  220. <View className="absolute top-8 left-7 ">
  221. <Pressable
  222. onPress={() => {
  223. if (router.canGoBack()) {
  224. router.back();
  225. } else {
  226. router.replace('./');
  227. }
  228. }}
  229. >
  230. <PreviousPageSvg />
  231. </Pressable>
  232. </View>
  233. </View>
  234. <View className="flex-column mx-[5%] mt-[5%]">
  235. <View>
  236. <Text className="text-3xl ">{chargeStationName}</Text>
  237. </View>
  238. <View className="flex-row justify-between items-center">
  239. <Text className="text-base" style={{ color: '#888888' }}>
  240. {chargeStationAddress}
  241. </Text>
  242. <NormalButton
  243. title={
  244. <View className="flex-row items-center justify-center text-center space-x-1">
  245. <DirectionLogoSvg />
  246. <Text className="text-base ">路線</Text>
  247. </View>
  248. }
  249. onPress={() => handleNavigationPress(stationLat, stationLng)}
  250. extendedStyle={{
  251. backgroundColor: '#E3F2F8',
  252. borderRadius: 61,
  253. paddingHorizontal: 20,
  254. paddingVertical: 6
  255. }}
  256. />
  257. </View>
  258. <View className="flex-row space-x-2 items-center pb-4 ">
  259. <CheckMarkLogoSvg />
  260. <Text>Walk-In</Text>
  261. {/* <Text>{distance} </Text> */}
  262. </View>
  263. <View
  264. className="flex-1 flex-row min-h-[20px] border-slate-300 my-6 rounded-2xl"
  265. style={{ borderWidth: 1 }}
  266. >
  267. <View className="flex-1 m-4">
  268. <View className="flex-1 flex-row ">
  269. <View className=" flex-1 flex-column ustify-between">
  270. <Text className="text-xl " style={styles.text}>
  271. 收費
  272. </Text>
  273. <View className="flex-row items-center space-x-2">
  274. <Text className="text-3xl text-[#02677D]">${price}</Text>
  275. <Text style={styles.text}>每度電</Text>
  276. </View>
  277. </View>
  278. <View className="items-center justify-center">
  279. <View className="w-[1px] h-[60%] bg-[#CCCCCC]" />
  280. </View>
  281. <View className=" flex-1 pl-4 flex-column justify-between">
  282. <Text className="text-xl " style={styles.text}>
  283. 現時可用充電槍數目
  284. </Text>
  285. <View className="flex-row items-center space-x-2">
  286. {isLoading ? (
  287. <Text>Loading...</Text>
  288. ) : (
  289. // <ActivityIndicator />
  290. <Text className="text-3xl text-[#02677D]">
  291. {
  292. newAvailableConnectors.find(
  293. (station: any) => station.stationID === chargeStationID
  294. )?.availableConnectors
  295. }
  296. </Text>
  297. )}
  298. </View>
  299. </View>
  300. </View>
  301. </View>
  302. </View>
  303. </View>
  304. <View className="min-h-[300px]">
  305. <Text className="text-xl pb-2 mx-[5%]" style={styles.text}>
  306. 充電站資訊
  307. </Text>
  308. <ChargingStationTabView titles={['收費詳情', '其他']} pricemodel_id={pricemodel_id} />
  309. </View>
  310. </ScrollView>
  311. </SafeAreaView>
  312. );
  313. };
  314. export default ResultDetailPageComponent;
  315. const styles = StyleSheet.create({
  316. text: {
  317. fontWeight: 300,
  318. color: '#000000'
  319. },
  320. leftLable: {
  321. fontSize: 17,
  322. color:'#000000',
  323. },
  324. rightLable: {
  325. fontSize: 17
  326. },
  327. });