searchResultComponent.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496
  1. import {
  2. View,
  3. Text,
  4. StyleSheet,
  5. Pressable,
  6. Image,
  7. ImageSourcePropType,
  8. TouchableWithoutFeedback,
  9. Keyboard,
  10. ActivityIndicator
  11. } from 'react-native';
  12. import React, { useState, useEffect, useRef, useMemo } from 'react';
  13. import { SafeAreaView } from 'react-native-safe-area-context';
  14. import MapView, { Marker, Region } from 'react-native-maps';
  15. import * as Location from 'expo-location';
  16. import { router, useLocalSearchParams } from 'expo-router';
  17. import { ArrowIconSvg, CheckMarkLogoSvg } from '../global/SVG';
  18. import NormalInput from '../global/normal_input';
  19. import BottomSheet, { BottomSheetScrollView } from '@gorhom/bottom-sheet';
  20. import { chargeStationService } from '../../service/chargeStationService';
  21. import { PROVIDER_GOOGLE } from 'react-native-maps';
  22. import { calculateDistance } from '../global/distanceCalculator';
  23. interface TabItem {
  24. imgURL: ImageSourcePropType;
  25. date: string;
  26. time: string;
  27. chargeStationName: string;
  28. chargeStationAddress: string;
  29. distance: string;
  30. stationID: string;
  31. lat: number;
  32. lng: number;
  33. }
  34. const dummyTabItems: TabItem[] = [
  35. {
  36. imgURL: require('../../assets/dummyStationPicture.png'),
  37. date: '今天',
  38. time: '16:30',
  39. chargeStationName: '觀塘偉業街充電站',
  40. chargeStationAddress: '九龍觀塘偉業街143號地下',
  41. distance: '400米',
  42. latitude: 22.31337,
  43. longitude: 114.21823
  44. },
  45. {
  46. imgURL: require('../../assets/dummyStationPicture5.jpeg'),
  47. date: '3月15',
  48. time: '17:45',
  49. chargeStationName: '香港沙頭角農莊',
  50. chargeStationAddress: '香港沙頭角農莊停車場',
  51. distance: '680米',
  52. latitude: 22.53898,
  53. longitude: 114.21319
  54. },
  55. {
  56. imgURL: require('../../assets/dummyStationPicture4.jpeg'),
  57. date: '3月15',
  58. time: '17:45',
  59. chargeStationName: '黃竹坑香葉道充電站',
  60. chargeStationAddress: '黃竹坑香葉道44號地下',
  61. distance: '680米',
  62. latitude: 22.24839,
  63. longitude: 114.16303
  64. }
  65. ];
  66. const SearchResultComponent = () => {
  67. const [region, setRegion] = useState<Region>({
  68. latitude: 22.302711, // Default to Hong Kong coordinates
  69. longitude: 114.177216,
  70. latitudeDelta: 0.01,
  71. longitudeDelta: 0.01
  72. });
  73. const [errorMsg, setErrorMsg] = useState<string | null>(null);
  74. const [searchInput, setSearchInput] = useState<string>('');
  75. const sheetRef = useRef<BottomSheet>(null);
  76. const snapPoints = useMemo(() => ['25%', '65%'], []);
  77. const mapRef = useRef<MapView>(null);
  78. const params = useLocalSearchParams();
  79. const [isLoading, setIsLoading] = useState(true);
  80. const [filteredItems, setFilteredItems] = useState<TabItem[]>([]);
  81. useEffect(() => {
  82. if (params.latitude && params.longitude) {
  83. setRegion({
  84. latitude: parseFloat(params.latitude as string),
  85. longitude: parseFloat(params.longitude as string),
  86. latitudeDelta: 0.01,
  87. longitudeDelta: 0.01
  88. });
  89. } else {
  90. (async () => {
  91. let { status } = await Location.requestForegroundPermissionsAsync();
  92. if (status !== 'granted') {
  93. setErrorMsg('Permission to access location was denied');
  94. return;
  95. }
  96. let myLocation = await Location.getLastKnownPositionAsync({});
  97. if (myLocation) {
  98. setRegion({
  99. latitude: myLocation.coords.latitude,
  100. longitude: myLocation.coords.longitude,
  101. latitudeDelta: 0.01,
  102. longitudeDelta: 0.01
  103. });
  104. }
  105. })();
  106. }
  107. }, []);
  108. useEffect(() => {
  109. if (mapRef.current && region) {
  110. mapRef.current.animateToRegion(region, 1000);
  111. }
  112. }, [region]);
  113. useEffect(() => {
  114. if (searchInput === '') {
  115. setFilteredItems([]);
  116. } else {
  117. const filteredData = dummyTabItems.filter((item) =>
  118. item.chargeStationName.includes(searchInput.toLocaleUpperCase())
  119. );
  120. setFilteredItems(filteredData);
  121. }
  122. }, [searchInput]);
  123. if (errorMsg) {
  124. return (
  125. <View className="flex-1 justify-center items-center ">
  126. <Text className="text-red-500">{errorMsg}</Text>
  127. </View>
  128. );
  129. }
  130. const handleRegionChange = (newRegion: Region) => {
  131. if (mapRef.current) {
  132. mapRef.current.animateToRegion(newRegion, 1000);
  133. }
  134. setRegion(newRegion);
  135. sheetRef.current?.snapToIndex(0);
  136. };
  137. // ************************************************************************************************
  138. const [currentLocation, setCurrentLocation] = useState<Location.LocationObject | null>(null);
  139. const [stations, setStations] = useState([]);
  140. const [tabItems, setTabItems] = useState<TabItem[]>([]);
  141. const getCurrentLocation = async () => {
  142. let { status } = await Location.requestForegroundPermissionsAsync();
  143. if (status !== 'granted') {
  144. console.error('Permission to access location was denied');
  145. return;
  146. }
  147. let location = await Location.getLastKnownPositionAsync({});
  148. setCurrentLocation(location);
  149. };
  150. useEffect(() => {
  151. getCurrentLocation();
  152. }, []);
  153. const fetchStations = async () => {
  154. setIsLoading(true);
  155. const fetchedStations = await chargeStationService.fetchChargeStations();
  156. setStations(fetchedStations);
  157. if (currentLocation) {
  158. const TabItems = await Promise.all(
  159. fetchedStations.map(async (station) => {
  160. // const distance = await calculateDistance(
  161. // Number(station.StationLat),
  162. // Number(station.StationLng),
  163. // currentLocation
  164. // );
  165. return {
  166. chargeStationAddress: station.Address,
  167. chargeStationName: station.StationName,
  168. lng: station.StationLng,
  169. lat: station.StationLat,
  170. date: '今天',
  171. stationID: station.StationID,
  172. // imgURL: stationImages[station.StationID] || require('../../assets/dummyStationPicture.png'),
  173. imgURL: station.image
  174. // distance: distance !== null ? formatDistance(distance) : 'N/A'
  175. };
  176. })
  177. );
  178. setTabItems(TabItems);
  179. setIsLoading(false);
  180. }
  181. };
  182. useEffect(() => {
  183. if (currentLocation) {
  184. fetchStations();
  185. }
  186. }, [currentLocation]);
  187. console.log('tabItems', tabItems);
  188. const formatDistance = (distanceInMeters: number): string => {
  189. if (distanceInMeters < 1000) {
  190. return `${Math.round(distanceInMeters)}米`;
  191. } else {
  192. const distanceInKm = distanceInMeters / 1000;
  193. return `${distanceInKm.toFixed(1)}公里`;
  194. }
  195. };
  196. const stationImages = {
  197. //沙頭角
  198. '2501161430118231000': require('../../assets/dummyStationPicture5.jpeg'),
  199. //黃竹坑
  200. '2411291610329331000': require('../../assets/dummyStationPicture4.jpeg'),
  201. //觀塘
  202. '2405311022116801000': require('../../assets/dummyStationPicture.png')
  203. };
  204. // const imageSource = stationImages[stationID] || require('../../assets/dummyStationPicture.png');
  205. // ************************************************************************************************************************************************
  206. return (
  207. <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
  208. <SafeAreaView className="flex-1" edges={['top', 'left', 'right']}>
  209. <View className="flex-1 relative">
  210. <View
  211. style={{
  212. position: 'absolute',
  213. top: 10,
  214. left: 10,
  215. right: 10,
  216. zIndex: 1,
  217. backgroundColor: 'transparent',
  218. alignItems: 'center'
  219. }}
  220. >
  221. <View className=" flex-1 flex-row bg-white rounded-xl">
  222. <Pressable
  223. style={styles.leftArrowBackButton}
  224. onPress={() => {
  225. if (router.canGoBack()) {
  226. router.back();
  227. } else {
  228. router.replace('/(auth)/(tabs)/(home)');
  229. }
  230. }}
  231. >
  232. <ArrowIconSvg />
  233. </Pressable>
  234. <NormalInput
  235. placeholder="搜尋這裡"
  236. onChangeText={(text) => {
  237. setSearchInput(text);
  238. console.log(text);
  239. }}
  240. extendedStyle={styles.textInput}
  241. />
  242. </View>
  243. {filteredItems.length > 0 && (
  244. <View style={styles.dropdown}>
  245. <View>
  246. {filteredItems.map((item, index) => (
  247. <Pressable
  248. key={index}
  249. onPress={() => {
  250. setSearchInput(item.chargeStationName);
  251. setFilteredItems([]);
  252. handleRegionChange({
  253. latitude: item.lat,
  254. longitude: item.lng,
  255. latitudeDelta: 0.01,
  256. longitudeDelta: 0.01
  257. });
  258. }}
  259. style={({ pressed }) => [
  260. styles.dropdownItem,
  261. pressed && styles.dropdownItemPress
  262. ]}
  263. >
  264. <Text>{item.chargeStationName}</Text>
  265. </Pressable>
  266. ))}
  267. </View>
  268. </View>
  269. )}
  270. </View>
  271. <MapView
  272. ref={mapRef}
  273. provider={PROVIDER_GOOGLE}
  274. style={styles.map}
  275. region={region}
  276. // initialRegion={region}
  277. cameraZoomRange={{
  278. minCenterCoordinateDistance: 500,
  279. maxCenterCoordinateDistance: 90000,
  280. animated: true
  281. }}
  282. showsUserLocation={true}
  283. showsMyLocationButton={false}
  284. >
  285. {tabItems.map((item, index) => (
  286. <Marker
  287. key={index}
  288. coordinate={{
  289. latitude: item.lat,
  290. longitude: item.lng
  291. }}
  292. title={item.chargeStationName}
  293. description={item.chargeStationAddress}
  294. />
  295. ))}
  296. </MapView>
  297. <BottomSheet ref={sheetRef} index={0} snapPoints={snapPoints}>
  298. <BottomSheetScrollView contentContainerStyle={styles.contentContainer}>
  299. <View className="flex-1 mx-[5%]">
  300. {isLoading ? (
  301. <View className="pt-14">
  302. <ActivityIndicator color="#34657b" />
  303. </View>
  304. ) : (
  305. tabItems
  306. .filter((item) => item.chargeStationName.includes(searchInput.toUpperCase()))
  307. .map((item, index) => {
  308. return (
  309. <Pressable
  310. key={index}
  311. onPress={() => {
  312. handleRegionChange({
  313. latitude: item.lat,
  314. longitude: item.lng,
  315. latitudeDelta: 0.01,
  316. longitudeDelta: 0.01
  317. });
  318. router.push({
  319. pathname: '/resultDetailPage',
  320. params: {
  321. imageSource: item.imgURL,
  322. chargeStationAddress: item.chargeStationAddress,
  323. chargeStationID: item.stationID,
  324. chargeStationName: item.chargeStationName,
  325. stationLat: item.lat,
  326. stationLng: item.lng
  327. }
  328. });
  329. }}
  330. style={({ pressed }) => [
  331. styles.container,
  332. {
  333. backgroundColor: pressed ? '#e7f2f8' : '#ffffff'
  334. }
  335. ]}
  336. >
  337. <View style={styles.rowContainer}>
  338. <Image style={styles.image} source={{ uri: item.imgURL }} />
  339. <View style={styles.textContainer}>
  340. <Text
  341. style={{
  342. fontWeight: '400',
  343. fontSize: 18,
  344. color: '#222222'
  345. }}
  346. >
  347. {item.chargeStationName}
  348. </Text>
  349. <Text
  350. style={{
  351. fontWeight: '400',
  352. fontSize: 14,
  353. color: '#888888'
  354. }}
  355. >
  356. {item.chargeStationAddress}
  357. </Text>
  358. <View className="flex-row space-x-2 items-center">
  359. <CheckMarkLogoSvg />
  360. <Text
  361. style={{
  362. fontWeight: '400',
  363. fontSize: 14,
  364. color: '#222222'
  365. }}
  366. >
  367. Walk-in
  368. </Text>
  369. </View>
  370. </View>
  371. {/* <Text
  372. style={{
  373. fontWeight: '400',
  374. fontSize: 16,
  375. color: '#888888',
  376. marginTop: 22
  377. }}
  378. className="flex-1 text-right"
  379. >
  380. {item.distance}
  381. </Text> */}
  382. </View>
  383. </Pressable>
  384. );
  385. })
  386. )}
  387. </View>
  388. </BottomSheetScrollView>
  389. </BottomSheet>
  390. </View>
  391. </SafeAreaView>
  392. </TouchableWithoutFeedback>
  393. );
  394. };
  395. export default SearchResultComponent;
  396. const styles = StyleSheet.create({
  397. container: {
  398. flex: 1
  399. },
  400. map: {
  401. flex: 1,
  402. width: '100%',
  403. height: '100%'
  404. },
  405. contentContainer: {
  406. backgroundColor: 'white'
  407. },
  408. itemContainer: {
  409. padding: 6,
  410. margin: 6,
  411. backgroundColor: '#eee'
  412. },
  413. image: {
  414. width: 100,
  415. height: 100,
  416. marginTop: 15,
  417. marginRight: 15,
  418. borderRadius: 10
  419. },
  420. textContainer: { flexDirection: 'column', gap: 8, marginTop: 22 },
  421. rowContainer: { flexDirection: 'row' },
  422. textInput: {
  423. width: '85%',
  424. maxWidth: '100%',
  425. fontSize: 16,
  426. padding: 20,
  427. paddingLeft: 0,
  428. borderLeftWidth: 0,
  429. borderTopWidth: 1,
  430. borderBottomWidth: 1,
  431. borderRightWidth: 1,
  432. borderBottomRightRadius: 12,
  433. borderTopRightRadius: 12,
  434. borderRadius: 0,
  435. borderColor: '#bbbbbb'
  436. },
  437. leftArrowBackButton: {
  438. width: '15%',
  439. maxWidth: '100%',
  440. fontSize: 16,
  441. padding: 20,
  442. paddingLeft: 30,
  443. borderBottomLeftRadius: 12,
  444. borderTopLeftRadius: 12,
  445. borderColor: '#bbbbbb',
  446. borderTopWidth: 1,
  447. borderBottomWidth: 1,
  448. borderLeftWidth: 1,
  449. alignItems: 'center',
  450. justifyContent: 'center'
  451. },
  452. dropdown: {
  453. backgroundColor: 'white',
  454. borderBottomLeftRadius: 12,
  455. borderBottomRightRadius: 12,
  456. borderLeftWidth: 1,
  457. borderRightWidth: 1,
  458. borderBottomWidth: 1,
  459. marginTop: 10,
  460. maxHeight: 200,
  461. width: '100%',
  462. position: 'absolute',
  463. top: 50,
  464. zIndex: 2,
  465. borderColor: '#bbbbbb'
  466. },
  467. dropdownItem: {
  468. padding: 10,
  469. borderBottomWidth: 1,
  470. borderBottomColor: '#ddd'
  471. },
  472. dropdownItemPress: {
  473. backgroundColor: '#e8f8fc'
  474. }
  475. });