|
|
@@ -0,0 +1,421 @@
|
|
|
+import {
|
|
|
+ View,
|
|
|
+ Text,
|
|
|
+ StyleSheet,
|
|
|
+ Pressable,
|
|
|
+ Image,
|
|
|
+ ImageSourcePropType,
|
|
|
+ TouchableWithoutFeedback,
|
|
|
+ Keyboard
|
|
|
+} from 'react-native';
|
|
|
+import React, { useState, useEffect, useRef, useMemo } from 'react';
|
|
|
+import { SafeAreaView } from 'react-native-safe-area-context';
|
|
|
+import MapView, { Marker, Region } from 'react-native-maps';
|
|
|
+import * as Location from 'expo-location';
|
|
|
+import { router, useLocalSearchParams } from 'expo-router';
|
|
|
+import { ArrowIconSvg, CheckMarkLogoSvg } from '../global/SVG';
|
|
|
+import NormalInput from '../global/normal_input';
|
|
|
+import BottomSheet, { BottomSheetScrollView } from '@gorhom/bottom-sheet';
|
|
|
+
|
|
|
+interface TabItem {
|
|
|
+ imgURL: ImageSourcePropType;
|
|
|
+ date: string;
|
|
|
+ time: string;
|
|
|
+ chargeStationName: string;
|
|
|
+ chargeStationAddress: string;
|
|
|
+ distance: string;
|
|
|
+ latitude: number;
|
|
|
+ longitude: number;
|
|
|
+}
|
|
|
+
|
|
|
+const dummyTabItems: TabItem[] = [
|
|
|
+ {
|
|
|
+ imgURL: require('../../assets/dummyStationPicture.png'),
|
|
|
+ date: '今天',
|
|
|
+ time: '16:30',
|
|
|
+ chargeStationName: '觀塘偉業街充電站',
|
|
|
+ chargeStationAddress: '九龍觀塘偉業街143號地下',
|
|
|
+ distance: '400米',
|
|
|
+ latitude: 22.310958,
|
|
|
+ longitude: 114.226065
|
|
|
+ },
|
|
|
+ {
|
|
|
+ imgURL: require('../../assets/dummyStationPicture2.png'),
|
|
|
+ date: '3月15',
|
|
|
+ time: '17:45',
|
|
|
+ chargeStationName: '中環IFC充電站',
|
|
|
+ chargeStationAddress: '香港中環皇后大道中999號',
|
|
|
+ distance: '680米',
|
|
|
+ latitude: 22.28552,
|
|
|
+ longitude: 114.15769
|
|
|
+ }
|
|
|
+];
|
|
|
+
|
|
|
+// **************************************
|
|
|
+// TODO: PUT THE GOOGLE MAP API KEY in ENV
|
|
|
+const GOOGLE_API_KEY = 'AIzaSyDYVSNuXFDNOhZAKfqeSwBTc8Pa7hKma1A';
|
|
|
+// ***************************************
|
|
|
+
|
|
|
+const SearchResultComponent = () => {
|
|
|
+ const [region, setRegion] = useState<Region | undefined>(undefined);
|
|
|
+ const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
|
|
+ const [searchInput, setSearchInput] = useState<string>('');
|
|
|
+ const sheetRef = useRef<BottomSheet>(null);
|
|
|
+ const snapPoints = useMemo(() => ['25%', '65%'], []);
|
|
|
+ const mapRef = useRef<MapView>(null);
|
|
|
+ const params = useLocalSearchParams();
|
|
|
+ const [filteredItems, setFilteredItems] = useState<TabItem[]>([]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (searchInput === '') {
|
|
|
+ setFilteredItems([]);
|
|
|
+ } else {
|
|
|
+ const filteredData = dummyTabItems.filter((item) =>
|
|
|
+ item.chargeStationName.includes(searchInput.toLocaleUpperCase())
|
|
|
+ );
|
|
|
+ setFilteredItems(filteredData);
|
|
|
+ }
|
|
|
+ }, [searchInput]);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (params.latitude && params.longitude) {
|
|
|
+ setRegion({
|
|
|
+ latitude: parseFloat(params.latitude as string),
|
|
|
+ longitude: parseFloat(params.longitude as string),
|
|
|
+ latitudeDelta: 0.01,
|
|
|
+ longitudeDelta: 0.01
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ (async () => {
|
|
|
+ let { status } =
|
|
|
+ await Location.requestForegroundPermissionsAsync();
|
|
|
+ if (status !== 'granted') {
|
|
|
+ setErrorMsg('Permission to access location was denied');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ let myLocation = await Location.getLastKnownPositionAsync({});
|
|
|
+ if (myLocation) {
|
|
|
+ setRegion({
|
|
|
+ latitude: myLocation.coords.latitude,
|
|
|
+ longitude: myLocation.coords.longitude,
|
|
|
+ latitudeDelta: 0.01,
|
|
|
+ longitudeDelta: 0.01
|
|
|
+ });
|
|
|
+ }
|
|
|
+ })();
|
|
|
+ }
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ useEffect(() => {
|
|
|
+ if (mapRef.current && region) {
|
|
|
+ mapRef.current.animateToRegion(region, 1000);
|
|
|
+ }
|
|
|
+ }, [region]);
|
|
|
+
|
|
|
+ if (errorMsg) {
|
|
|
+ return (
|
|
|
+ <View className="flex-1 justify-center items-center ">
|
|
|
+ <Text className="text-red-500">{errorMsg}</Text>
|
|
|
+ </View>
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ const handleRegionChange = (newRegion: Region) => {
|
|
|
+ if (mapRef.current) {
|
|
|
+ mapRef.current.animateToRegion(newRegion, 1000);
|
|
|
+ }
|
|
|
+ setRegion(newRegion);
|
|
|
+ sheetRef.current?.snapToIndex(0);
|
|
|
+ };
|
|
|
+
|
|
|
+ return (
|
|
|
+ <TouchableWithoutFeedback onPress={Keyboard.dismiss}>
|
|
|
+ <SafeAreaView className="flex-1" edges={['top', 'left', 'right']}>
|
|
|
+ <View className="flex-1 relative">
|
|
|
+ <View
|
|
|
+ style={{
|
|
|
+ position: 'absolute',
|
|
|
+ top: 10,
|
|
|
+ left: 10,
|
|
|
+ right: 10,
|
|
|
+ zIndex: 1,
|
|
|
+ backgroundColor: 'transparent',
|
|
|
+ alignItems: 'center'
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <View className=" flex-1 flex-row bg-white rounded-xl">
|
|
|
+ <Pressable
|
|
|
+ style={styles.leftArrowBackButton}
|
|
|
+ onPress={() => {
|
|
|
+ if (router.canGoBack()) {
|
|
|
+ router.back();
|
|
|
+ } else {
|
|
|
+ router.replace('/(auth)/(tabs)/(home)');
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ <ArrowIconSvg />
|
|
|
+ </Pressable>
|
|
|
+ <NormalInput
|
|
|
+ placeholder="搜尋這裡"
|
|
|
+ onChangeText={(text) => {
|
|
|
+ setSearchInput(text);
|
|
|
+ console.log(text);
|
|
|
+ }}
|
|
|
+ extendedStyle={styles.textInput}
|
|
|
+ />
|
|
|
+ </View>
|
|
|
+ {filteredItems.length > 0 && (
|
|
|
+ <View style={styles.dropdown}>
|
|
|
+ <View>
|
|
|
+ {filteredItems.map((item, index) => (
|
|
|
+ <Pressable
|
|
|
+ key={index}
|
|
|
+ onPress={() => {
|
|
|
+ setSearchInput(
|
|
|
+ item.chargeStationName
|
|
|
+ );
|
|
|
+ setFilteredItems([]);
|
|
|
+ handleRegionChange({
|
|
|
+ latitude: item.latitude,
|
|
|
+ longitude: item.longitude,
|
|
|
+ latitudeDelta: 0.01,
|
|
|
+ longitudeDelta: 0.01
|
|
|
+ });
|
|
|
+ }}
|
|
|
+ style={({ pressed }) => [
|
|
|
+ styles.dropdownItem,
|
|
|
+ pressed &&
|
|
|
+ styles.dropdownItemPress
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <Text>
|
|
|
+ {item.chargeStationName}
|
|
|
+ </Text>
|
|
|
+ </Pressable>
|
|
|
+ ))}
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ )}
|
|
|
+ </View>
|
|
|
+ <MapView
|
|
|
+ ref={mapRef}
|
|
|
+ style={styles.map}
|
|
|
+ region={region}
|
|
|
+ cameraZoomRange={{
|
|
|
+ minCenterCoordinateDistance: 500,
|
|
|
+ maxCenterCoordinateDistance: 90000,
|
|
|
+ animated: true
|
|
|
+ }}
|
|
|
+ showsUserLocation={true}
|
|
|
+ showsMyLocationButton={false}
|
|
|
+ >
|
|
|
+ {dummyTabItems.map((item, index) => (
|
|
|
+ <Marker
|
|
|
+ key={index}
|
|
|
+ coordinate={{
|
|
|
+ latitude: item.latitude,
|
|
|
+ longitude: item.longitude
|
|
|
+ }}
|
|
|
+ title={item.chargeStationName}
|
|
|
+ description={item.chargeStationAddress}
|
|
|
+ />
|
|
|
+ ))}
|
|
|
+ </MapView>
|
|
|
+ <BottomSheet
|
|
|
+ ref={sheetRef}
|
|
|
+ index={0}
|
|
|
+ snapPoints={snapPoints}
|
|
|
+ >
|
|
|
+ <BottomSheetScrollView
|
|
|
+ contentContainerStyle={styles.contentContainer}
|
|
|
+ >
|
|
|
+ <View className="flex-1 mx-[5%]">
|
|
|
+ {dummyTabItems
|
|
|
+ .filter((item) =>
|
|
|
+ item.chargeStationName.includes(
|
|
|
+ searchInput.toUpperCase()
|
|
|
+ )
|
|
|
+ )
|
|
|
+ .map((item, index) => {
|
|
|
+ return (
|
|
|
+ <Pressable
|
|
|
+ key={index}
|
|
|
+ onPress={() => {
|
|
|
+ handleRegionChange({
|
|
|
+ latitude: item.latitude,
|
|
|
+ longitude:
|
|
|
+ item.longitude,
|
|
|
+ latitudeDelta: 0.01,
|
|
|
+ longitudeDelta: 0.01
|
|
|
+ });
|
|
|
+ router.push(
|
|
|
+ '/resultDetailPage'
|
|
|
+ );
|
|
|
+ }}
|
|
|
+ style={({ pressed }) => [
|
|
|
+ styles.container,
|
|
|
+ {
|
|
|
+ backgroundColor: pressed
|
|
|
+ ? '#e7f2f8'
|
|
|
+ : '#ffffff'
|
|
|
+ }
|
|
|
+ ]}
|
|
|
+ >
|
|
|
+ <View
|
|
|
+ style={styles.rowContainer}
|
|
|
+ >
|
|
|
+ <Image
|
|
|
+ style={styles.image}
|
|
|
+ source={item.imgURL}
|
|
|
+ />
|
|
|
+ <View
|
|
|
+ style={
|
|
|
+ styles.textContainer
|
|
|
+ }
|
|
|
+ >
|
|
|
+ <Text
|
|
|
+ style={{
|
|
|
+ fontWeight:
|
|
|
+ '400',
|
|
|
+ fontSize: 18,
|
|
|
+ color: '#222222'
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {
|
|
|
+ item.chargeStationName
|
|
|
+ }
|
|
|
+ </Text>
|
|
|
+ <Text
|
|
|
+ style={{
|
|
|
+ fontWeight:
|
|
|
+ '400',
|
|
|
+ fontSize: 14,
|
|
|
+ color: '#888888'
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ {
|
|
|
+ item.chargeStationAddress
|
|
|
+ }
|
|
|
+ </Text>
|
|
|
+ <View className="flex-row space-x-2 items-center">
|
|
|
+ <CheckMarkLogoSvg />
|
|
|
+ <Text
|
|
|
+ style={{
|
|
|
+ fontWeight:
|
|
|
+ '400',
|
|
|
+ fontSize: 14,
|
|
|
+ color: '#222222'
|
|
|
+ }}
|
|
|
+ >
|
|
|
+ Walk-in
|
|
|
+ </Text>
|
|
|
+ </View>
|
|
|
+ </View>
|
|
|
+ <Text
|
|
|
+ style={{
|
|
|
+ fontWeight: '400',
|
|
|
+ fontSize: 16,
|
|
|
+ color: '#888888',
|
|
|
+ marginTop: 22
|
|
|
+ }}
|
|
|
+ className="flex-1 text-right"
|
|
|
+ >
|
|
|
+ {item.distance}
|
|
|
+ </Text>
|
|
|
+ </View>
|
|
|
+ </Pressable>
|
|
|
+ );
|
|
|
+ })}
|
|
|
+ </View>
|
|
|
+ </BottomSheetScrollView>
|
|
|
+ </BottomSheet>
|
|
|
+ </View>
|
|
|
+ </SafeAreaView>
|
|
|
+ </TouchableWithoutFeedback>
|
|
|
+ );
|
|
|
+};
|
|
|
+
|
|
|
+export default SearchResultComponent;
|
|
|
+
|
|
|
+const styles = StyleSheet.create({
|
|
|
+ container: {
|
|
|
+ flex: 1
|
|
|
+ },
|
|
|
+ map: {
|
|
|
+ flex: 1,
|
|
|
+ width: '100%',
|
|
|
+ height: '100%'
|
|
|
+ },
|
|
|
+ contentContainer: {
|
|
|
+ backgroundColor: 'white'
|
|
|
+ },
|
|
|
+ itemContainer: {
|
|
|
+ padding: 6,
|
|
|
+ margin: 6,
|
|
|
+ backgroundColor: '#eee'
|
|
|
+ },
|
|
|
+ image: {
|
|
|
+ width: 100,
|
|
|
+ height: 100,
|
|
|
+ marginTop: 15,
|
|
|
+ marginRight: 15,
|
|
|
+
|
|
|
+ borderRadius: 10
|
|
|
+ },
|
|
|
+ textContainer: { flexDirection: 'column', gap: 8, marginTop: 22 },
|
|
|
+ rowContainer: { flexDirection: 'row' },
|
|
|
+ textInput: {
|
|
|
+ width: '85%',
|
|
|
+ maxWidth: '100%',
|
|
|
+ fontSize: 16,
|
|
|
+ padding: 20,
|
|
|
+ paddingLeft: 0,
|
|
|
+ borderLeftWidth: 0,
|
|
|
+ borderTopWidth: 1,
|
|
|
+ borderBottomWidth: 1,
|
|
|
+ borderRightWidth: 1,
|
|
|
+ borderBottomRightRadius: 12,
|
|
|
+ borderTopRightRadius: 12,
|
|
|
+ borderRadius: 0,
|
|
|
+ borderColor: '#bbbbbb'
|
|
|
+ },
|
|
|
+ leftArrowBackButton: {
|
|
|
+ width: '15%',
|
|
|
+ maxWidth: '100%',
|
|
|
+ fontSize: 16,
|
|
|
+ padding: 20,
|
|
|
+ paddingLeft: 30,
|
|
|
+ borderBottomLeftRadius: 12,
|
|
|
+ borderTopLeftRadius: 12,
|
|
|
+ borderColor: '#bbbbbb',
|
|
|
+ borderTopWidth: 1,
|
|
|
+ borderBottomWidth: 1,
|
|
|
+ borderLeftWidth: 1,
|
|
|
+ alignItems: 'center',
|
|
|
+ justifyContent: 'center'
|
|
|
+ },
|
|
|
+ dropdown: {
|
|
|
+ backgroundColor: 'white',
|
|
|
+ borderBottomLeftRadius: 12,
|
|
|
+ borderBottomRightRadius: 12,
|
|
|
+ borderLeftWidth: 1,
|
|
|
+ borderRightWidth: 1,
|
|
|
+ borderBottomWidth: 1,
|
|
|
+ marginTop: 10,
|
|
|
+ maxHeight: 200,
|
|
|
+ width: '100%',
|
|
|
+ position: 'absolute',
|
|
|
+ top: 50,
|
|
|
+ zIndex: 2,
|
|
|
+ borderColor: '#bbbbbb'
|
|
|
+ },
|
|
|
+ dropdownItem: {
|
|
|
+ padding: 10,
|
|
|
+ borderBottomWidth: 1,
|
|
|
+ borderBottomColor: '#ddd'
|
|
|
+ },
|
|
|
+ dropdownItemPress: {
|
|
|
+ backgroundColor: '#e8f8fc'
|
|
|
+ }
|
|
|
+});
|