searchResultComponent.tsx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. import {
  2. View,
  3. Text,
  4. StyleSheet,
  5. Pressable,
  6. Image,
  7. ImageSourcePropType,
  8. TouchableWithoutFeedback,
  9. Keyboard
  10. } from 'react-native';
  11. import React, { useState, useEffect, useRef, useMemo } from 'react';
  12. import { SafeAreaView } from 'react-native-safe-area-context';
  13. import MapView, { Marker, Region } from 'react-native-maps';
  14. import * as Location from 'expo-location';
  15. import { router, useLocalSearchParams } from 'expo-router';
  16. import { ArrowIconSvg, CheckMarkLogoSvg } from '../global/SVG';
  17. import NormalInput from '../global/normal_input';
  18. import BottomSheet, { BottomSheetScrollView } from '@gorhom/bottom-sheet';
  19. interface TabItem {
  20. imgURL: ImageSourcePropType;
  21. date: string;
  22. time: string;
  23. chargeStationName: string;
  24. chargeStationAddress: string;
  25. distance: string;
  26. latitude: number;
  27. longitude: number;
  28. }
  29. const dummyTabItems: TabItem[] = [
  30. {
  31. imgURL: require('../../assets/dummyStationPicture.png'),
  32. date: '今天',
  33. time: '16:30',
  34. chargeStationName: '觀塘偉業街充電站',
  35. chargeStationAddress: '九龍觀塘偉業街143號地下',
  36. distance: '400米',
  37. latitude: 22.310958,
  38. longitude: 114.226065
  39. },
  40. {
  41. imgURL: require('../../assets/dummyStationPicture2.png'),
  42. date: '3月15',
  43. time: '17:45',
  44. chargeStationName: '中環IFC充電站',
  45. chargeStationAddress: '香港中環皇后大道中999號',
  46. distance: '680米',
  47. latitude: 22.28552,
  48. longitude: 114.15769
  49. }
  50. ];
  51. // **************************************
  52. // TODO: PUT THE GOOGLE MAP API KEY in ENV
  53. const GOOGLE_API_KEY = 'AIzaSyDYVSNuXFDNOhZAKfqeSwBTc8Pa7hKma1A';
  54. // ***************************************
  55. const SearchResultComponent = () => {
  56. const [region, setRegion] = useState<Region | undefined>(undefined);
  57. const [errorMsg, setErrorMsg] = useState<string | null>(null);
  58. const [searchInput, setSearchInput] = useState<string>('');
  59. const sheetRef = useRef<BottomSheet>(null);
  60. const snapPoints = useMemo(() => ['25%', '65%'], []);
  61. const mapRef = useRef<MapView>(null);
  62. const params = useLocalSearchParams();
  63. const [filteredItems, setFilteredItems] = useState<TabItem[]>([]);
  64. useEffect(() => {
  65. if (searchInput === '') {
  66. setFilteredItems([]);
  67. } else {
  68. const filteredData = dummyTabItems.filter((item) =>
  69. item.chargeStationName.includes(searchInput.toLocaleUpperCase())
  70. );
  71. setFilteredItems(filteredData);
  72. }
  73. }, [searchInput]);
  74. useEffect(() => {
  75. if (params.latitude && params.longitude) {
  76. setRegion({
  77. latitude: parseFloat(params.latitude as string),
  78. longitude: parseFloat(params.longitude as string),
  79. latitudeDelta: 0.01,
  80. longitudeDelta: 0.01
  81. });
  82. } else {
  83. (async () => {
  84. let { status } =
  85. await Location.requestForegroundPermissionsAsync();
  86. if (status !== 'granted') {
  87. setErrorMsg('Permission to access location was denied');
  88. return;
  89. }
  90. let myLocation = await Location.getLastKnownPositionAsync({});
  91. if (myLocation) {
  92. setRegion({
  93. latitude: myLocation.coords.latitude,
  94. longitude: myLocation.coords.longitude,
  95. latitudeDelta: 0.01,
  96. longitudeDelta: 0.01
  97. });
  98. }
  99. })();
  100. }
  101. }, []);
  102. useEffect(() => {
  103. if (mapRef.current && region) {
  104. mapRef.current.animateToRegion(region, 1000);
  105. }
  106. }, [region]);
  107. if (errorMsg) {
  108. return (
  109. <View className="flex-1 justify-center items-center ">
  110. <Text className="text-red-500">{errorMsg}</Text>
  111. </View>
  112. );
  113. }
  114. const handleRegionChange = (newRegion: Region) => {
  115. if (mapRef.current) {
  116. mapRef.current.animateToRegion(newRegion, 1000);
  117. }
  118. setRegion(newRegion);
  119. sheetRef.current?.snapToIndex(0);
  120. };
  121. return (
  122. <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
  123. <SafeAreaView className="flex-1" edges={['top', 'left', 'right']}>
  124. <View className="flex-1 relative">
  125. <View
  126. style={{
  127. position: 'absolute',
  128. top: 10,
  129. left: 10,
  130. right: 10,
  131. zIndex: 1,
  132. backgroundColor: 'transparent',
  133. alignItems: 'center'
  134. }}
  135. >
  136. <View className=" flex-1 flex-row bg-white rounded-xl">
  137. <Pressable
  138. style={styles.leftArrowBackButton}
  139. onPress={() => {
  140. if (router.canGoBack()) {
  141. router.back();
  142. } else {
  143. router.replace('/(auth)/(tabs)/(home)');
  144. }
  145. }}
  146. >
  147. <ArrowIconSvg />
  148. </Pressable>
  149. <NormalInput
  150. placeholder="搜尋這裡"
  151. onChangeText={(text) => {
  152. setSearchInput(text);
  153. console.log(text);
  154. }}
  155. extendedStyle={styles.textInput}
  156. />
  157. </View>
  158. {filteredItems.length > 0 && (
  159. <View style={styles.dropdown}>
  160. <View>
  161. {filteredItems.map((item, index) => (
  162. <Pressable
  163. key={index}
  164. onPress={() => {
  165. setSearchInput(
  166. item.chargeStationName
  167. );
  168. setFilteredItems([]);
  169. handleRegionChange({
  170. latitude: item.latitude,
  171. longitude: item.longitude,
  172. latitudeDelta: 0.01,
  173. longitudeDelta: 0.01
  174. });
  175. }}
  176. style={({ pressed }) => [
  177. styles.dropdownItem,
  178. pressed &&
  179. styles.dropdownItemPress
  180. ]}
  181. >
  182. <Text>
  183. {item.chargeStationName}
  184. </Text>
  185. </Pressable>
  186. ))}
  187. </View>
  188. </View>
  189. )}
  190. </View>
  191. <MapView
  192. ref={mapRef}
  193. style={styles.map}
  194. region={region}
  195. cameraZoomRange={{
  196. minCenterCoordinateDistance: 500,
  197. maxCenterCoordinateDistance: 90000,
  198. animated: true
  199. }}
  200. showsUserLocation={true}
  201. showsMyLocationButton={false}
  202. >
  203. {dummyTabItems.map((item, index) => (
  204. <Marker
  205. key={index}
  206. coordinate={{
  207. latitude: item.latitude,
  208. longitude: item.longitude
  209. }}
  210. title={item.chargeStationName}
  211. description={item.chargeStationAddress}
  212. />
  213. ))}
  214. </MapView>
  215. <BottomSheet
  216. ref={sheetRef}
  217. index={0}
  218. snapPoints={snapPoints}
  219. >
  220. <BottomSheetScrollView
  221. contentContainerStyle={styles.contentContainer}
  222. >
  223. <View className="flex-1 mx-[5%]">
  224. {dummyTabItems
  225. .filter((item) =>
  226. item.chargeStationName.includes(
  227. searchInput.toUpperCase()
  228. )
  229. )
  230. .map((item, index) => {
  231. return (
  232. <Pressable
  233. key={index}
  234. onPress={() => {
  235. handleRegionChange({
  236. latitude: item.latitude,
  237. longitude:
  238. item.longitude,
  239. latitudeDelta: 0.01,
  240. longitudeDelta: 0.01
  241. });
  242. router.push(
  243. '/resultDetailPage'
  244. );
  245. }}
  246. style={({ pressed }) => [
  247. styles.container,
  248. {
  249. backgroundColor: pressed
  250. ? '#e7f2f8'
  251. : '#ffffff'
  252. }
  253. ]}
  254. >
  255. <View
  256. style={styles.rowContainer}
  257. >
  258. <Image
  259. style={styles.image}
  260. source={item.imgURL}
  261. />
  262. <View
  263. style={
  264. styles.textContainer
  265. }
  266. >
  267. <Text
  268. style={{
  269. fontWeight:
  270. '400',
  271. fontSize: 18,
  272. color: '#222222'
  273. }}
  274. >
  275. {
  276. item.chargeStationName
  277. }
  278. </Text>
  279. <Text
  280. style={{
  281. fontWeight:
  282. '400',
  283. fontSize: 14,
  284. color: '#888888'
  285. }}
  286. >
  287. {
  288. item.chargeStationAddress
  289. }
  290. </Text>
  291. <View className="flex-row space-x-2 items-center">
  292. <CheckMarkLogoSvg />
  293. <Text
  294. style={{
  295. fontWeight:
  296. '400',
  297. fontSize: 14,
  298. color: '#222222'
  299. }}
  300. >
  301. Walk-in
  302. </Text>
  303. </View>
  304. </View>
  305. <Text
  306. style={{
  307. fontWeight: '400',
  308. fontSize: 16,
  309. color: '#888888',
  310. marginTop: 22
  311. }}
  312. className="flex-1 text-right"
  313. >
  314. {item.distance}
  315. </Text>
  316. </View>
  317. </Pressable>
  318. );
  319. })}
  320. </View>
  321. </BottomSheetScrollView>
  322. </BottomSheet>
  323. </View>
  324. </SafeAreaView>
  325. </TouchableWithoutFeedback>
  326. );
  327. };
  328. export default SearchResultComponent;
  329. const styles = StyleSheet.create({
  330. container: {
  331. flex: 1
  332. },
  333. map: {
  334. flex: 1,
  335. width: '100%',
  336. height: '100%'
  337. },
  338. contentContainer: {
  339. backgroundColor: 'white'
  340. },
  341. itemContainer: {
  342. padding: 6,
  343. margin: 6,
  344. backgroundColor: '#eee'
  345. },
  346. image: {
  347. width: 100,
  348. height: 100,
  349. marginTop: 15,
  350. marginRight: 15,
  351. borderRadius: 10
  352. },
  353. textContainer: { flexDirection: 'column', gap: 8, marginTop: 22 },
  354. rowContainer: { flexDirection: 'row' },
  355. textInput: {
  356. width: '85%',
  357. maxWidth: '100%',
  358. fontSize: 16,
  359. padding: 20,
  360. paddingLeft: 0,
  361. borderLeftWidth: 0,
  362. borderTopWidth: 1,
  363. borderBottomWidth: 1,
  364. borderRightWidth: 1,
  365. borderBottomRightRadius: 12,
  366. borderTopRightRadius: 12,
  367. borderRadius: 0,
  368. borderColor: '#bbbbbb'
  369. },
  370. leftArrowBackButton: {
  371. width: '15%',
  372. maxWidth: '100%',
  373. fontSize: 16,
  374. padding: 20,
  375. paddingLeft: 30,
  376. borderBottomLeftRadius: 12,
  377. borderTopLeftRadius: 12,
  378. borderColor: '#bbbbbb',
  379. borderTopWidth: 1,
  380. borderBottomWidth: 1,
  381. borderLeftWidth: 1,
  382. alignItems: 'center',
  383. justifyContent: 'center'
  384. },
  385. dropdown: {
  386. backgroundColor: 'white',
  387. borderBottomLeftRadius: 12,
  388. borderBottomRightRadius: 12,
  389. borderLeftWidth: 1,
  390. borderRightWidth: 1,
  391. borderBottomWidth: 1,
  392. marginTop: 10,
  393. maxHeight: 200,
  394. width: '100%',
  395. position: 'absolute',
  396. top: 50,
  397. zIndex: 2,
  398. borderColor: '#bbbbbb'
  399. },
  400. dropdownItem: {
  401. padding: 10,
  402. borderBottomWidth: 1,
  403. borderBottomColor: '#ddd'
  404. },
  405. dropdownItemPress: {
  406. backgroundColor: '#e8f8fc'
  407. }
  408. });