homePage.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432
  1. import {
  2. View,
  3. Text,
  4. ScrollView,
  5. FlatList,
  6. Pressable,
  7. ActivityIndicator,
  8. Image,
  9. Modal,
  10. Alert,
  11. TextInput
  12. } from 'react-native';
  13. import NormalButton from '../global/normal_button';
  14. import { SafeAreaView } from 'react-native-safe-area-context';
  15. import { router, useFocusEffect } from 'expo-router';
  16. import { useColorScheme } from 'nativewind';
  17. import RecentlyBookedScrollView from '../global/recentlyBookedScrollView';
  18. import {
  19. BellIconSvg,
  20. HomeIconSvg,
  21. MyBookingIconSvg,
  22. WhatsAppSvg,
  23. WalletSvg,
  24. MyWalletSvg,
  25. QrCodeIconSvg,
  26. VipCodeIconSvg
  27. } from '../global/SVG';
  28. import { AuthContext } from '../../context/AuthProvider';
  29. import { useCallback, useContext, useEffect, useState } from 'react';
  30. import { authenticationService } from '../../service/authService';
  31. import { chargeStationService } from '../../service/chargeStationService';
  32. import { walletService } from '../../service/walletService';
  33. import useUserInfoStore from '../../providers/userinfo_store';
  34. import NormalInput from '../global/normal_input';
  35. import { notificationStorage } from '../notificationStorage';
  36. import { handleGoWhatsApp } from '../../util/index';
  37. interface HomePageProps {}
  38. const HomePage: React.FC<HomePageProps> = () => {
  39. const now = new Date();
  40. const { user } = useContext(AuthContext);
  41. const { userID, currentPrice, setUserID, setCurrentPrice, setNotifySessionID } = useUserInfoStore();
  42. const { colorScheme, toggleColorScheme } = useColorScheme();
  43. const [showLicencePlateMessage, setShowLicencePlateMessage] = useState<boolean>(false);
  44. const [licensePlate, setLicensePlate] = useState<string>('');
  45. const [showConfirmationModal, setShowConfirmationModal] = useState<boolean>(false);
  46. const [showOnboarding, setShowOnboarding] = useState(true);
  47. const [mainPromotion, setMainPromotion] = useState([]);
  48. const [mainPromotionImage, setMainPromotionImage] = useState('');
  49. const [reservationAfter2025, setReservationAfter2025] = useState([]);
  50. const [isLoadingReservations, setIsLoadingReservations] = useState(true);
  51. const [unreadCount, setUnreadCount] = useState(0);
  52. useEffect(() => {
  53. const fetchIDandCheckLicensePlate = async () => {
  54. try {
  55. const response = await authenticationService.getUserInfo();
  56. //if success, set user ID,
  57. if (response) {
  58. setNotifySessionID(response.data.notify_session_id);
  59. setUserID(response.data.id);
  60. //after setting id, also check if the user has a valid license plate, if not, show message
  61. if (!response.data.cars || !Array.isArray(response.data.cars)) {
  62. Alert.alert('無法檢測車輛資訊', '請稍後再試');
  63. setShowLicencePlateMessage(false);
  64. }
  65. if (response.data.cars.length === 1 && response.data.cars[0].license_plate === '0000') {
  66. setShowLicencePlateMessage(true);
  67. }
  68. } else {
  69. Alert.alert('fail to set user/notification session ID');
  70. }
  71. } catch (error) {
  72. console.log(error);
  73. }
  74. };
  75. const fetchCurrentPrice = async () => {
  76. try {
  77. const response = await chargeStationService.getCurrentPrice();
  78. if (response) {
  79. setCurrentPrice(response);
  80. }
  81. } catch (error) {
  82. console.log('main page fetch current price error', error);
  83. }
  84. };
  85. const fetchMainPromotion = async () => {
  86. try {
  87. const response = await chargeStationService.getAdvertise();
  88. if (response) {
  89. const mainPromo = response.filter((item: any) => item.is_main)[0];
  90. setMainPromotion(mainPromo);
  91. if (mainPromo) {
  92. const mainPromoImage = await chargeStationService.getProcessedImageUrl(mainPromo.image_url);
  93. if (mainPromoImage) {
  94. setMainPromotionImage(mainPromoImage);
  95. }
  96. }
  97. }
  98. } catch (error) {
  99. console.log('Error fetching promotion:', error);
  100. }
  101. };
  102. fetchMainPromotion();
  103. const fetchWithAllSettled = async () => {
  104. const results = await Promise.allSettled([
  105. fetchIDandCheckLicensePlate(),
  106. fetchCurrentPrice(),
  107. fetchMainPromotion()
  108. ]);
  109. };
  110. fetchWithAllSettled();
  111. }, []);
  112. useFocusEffect(
  113. useCallback(() => {
  114. let isActive = true;
  115. const fetchData = async () => {
  116. setIsLoadingReservations(true); // Start loading
  117. try {
  118. const results = await Promise.allSettled([
  119. chargeStationService.fetchReservationHistories(),
  120. chargeStationService.getAdvertise()
  121. ]);
  122. if (!isActive) return;
  123. // Handle reservation data
  124. if (results[0].status === 'fulfilled') {
  125. const year2025 = new Date('2025-02-01T00:00:00.000Z');
  126. const reservationAfter2025 = results[0].value.filter((r: any) => {
  127. const date = new Date(r.createdAt);
  128. return date > year2025;
  129. });
  130. setReservationAfter2025(reservationAfter2025);
  131. } else if (results[0].status === 'rejected') {
  132. Alert.alert('Error fetching reservations:', results[0].reason);
  133. }
  134. // Get viewed notifications
  135. const viewedNotifications = await notificationStorage.getViewedNotifications();
  136. let totalUnread = 0;
  137. // Count unread reservations
  138. if (results[0].status === 'fulfilled') {
  139. const unreadReservations = reservationAfter2025.filter((r: any) => {
  140. return !viewedNotifications.some((vn: any) => vn.id === r.id);
  141. });
  142. totalUnread += unreadReservations.length;
  143. }
  144. // Count unread promotions
  145. if (results[1].status === 'fulfilled') {
  146. const unreadPromotions = results[1].value.filter((p: any) => {
  147. return !viewedNotifications.some((vn) => vn.id === p.id);
  148. });
  149. totalUnread += unreadPromotions.length;
  150. }
  151. setUnreadCount(totalUnread);
  152. } catch (error) {
  153. if (!isActive) return;
  154. console.log('Error fetching data');
  155. } finally {
  156. if (isActive) {
  157. setIsLoadingReservations(false);
  158. }
  159. }
  160. };
  161. fetchData();
  162. console.log('iiiiiii');
  163. return () => {
  164. isActive = false;
  165. };
  166. }, [])
  167. );
  168. const saveLicensePlate = async (licensePlate: string) => {
  169. try {
  170. const response = await chargeStationService.addCar(
  171. licensePlate,
  172. '1834d087-bfc1-4f90-8f09-805e3d9422b5',
  173. 'f599470d-53a5-4026-99c0-2dab34c77f39',
  174. true
  175. );
  176. if (response === true) {
  177. console.log('License plate saved successfully');
  178. } else {
  179. Alert.alert('無法保存車牌號碼', '請稍後再試');
  180. }
  181. } catch (error) {
  182. Alert.alert('暫時無法保存車牌號碼', '請稍後再試');
  183. }
  184. };
  185. return (
  186. <SafeAreaView edges={['top', 'left', 'right']} className="flex-1 bg-white">
  187. {/* Add Modal component */}
  188. {mainPromotionImage && (
  189. <Modal
  190. animationType="fade"
  191. transparent={true}
  192. visible={showOnboarding}
  193. onRequestClose={() => setShowOnboarding(false)}
  194. >
  195. <Pressable
  196. className="flex-1 bg-black/50 items-center justify-center"
  197. onPress={() => setShowOnboarding(false)}
  198. >
  199. <View className="w-[120%] rounded-2xl ">
  200. <Image
  201. source={{ uri: mainPromotionImage }}
  202. className="w-full aspect-square "
  203. resizeMode="contain"
  204. />
  205. <Text className="text-center mt-4 mb-2 text-gray-200">點擊任意位置關閉</Text>
  206. </View>
  207. </Pressable>
  208. </Modal>
  209. )}
  210. {showLicencePlateMessage && (
  211. <Modal
  212. animationType="fade"
  213. transparent={true}
  214. visible={showLicencePlateMessage}
  215. onRequestClose={() => setShowLicencePlateMessage(false)}
  216. >
  217. <View className="flex-1 bg-black/50 items-center justify-center">
  218. {!showConfirmationModal ? (
  219. // License Plate Input Modal
  220. <View className="flex flex-col rounded-2xl bg-white overflow-hidden w-[80%]">
  221. <View className="bg-[#E3F2F8]">
  222. <Text className="text-base lg:text-lg font-[500] text-center p-4">
  223. 請添加您的車牌號碼
  224. </Text>
  225. </View>
  226. <View className="p-4 ">
  227. <Text className="text-sm lg:text-base font-[500] text-left mb-4">
  228. 為更好地為您提供服務,請在您的帳戶中添加車牌號碼。
  229. </Text>
  230. <NormalInput
  231. value={licensePlate}
  232. placeholder="車牌號碼"
  233. onChangeText={(s) => setLicensePlate(s)}
  234. extendedStyle={{ borderRadius: 12, marginBottom: 0 }}
  235. textContentType="none"
  236. autoComplete="off"
  237. keyboardType="default"
  238. />
  239. </View>
  240. <View className="pr-4 pl-4 pb-4 ">
  241. <NormalButton
  242. title={<Text className="text-white text-sm lg:text-lg">確定</Text>}
  243. onPress={() => {
  244. //here when users click confirm, i want to pop another modal that say you have entered "xxxxxx", click confirm to continue
  245. if (!licensePlate.trim()) {
  246. Alert.alert('請輸入車牌號碼');
  247. return;
  248. }
  249. if (licensePlate.trim().length < 4 || licensePlate.trim().length > 10) {
  250. Alert.alert('無效的車牌號碼', '請輸入有效的車牌號碼');
  251. return;
  252. }
  253. setShowConfirmationModal(true);
  254. }}
  255. />
  256. </View>
  257. </View>
  258. ) : (
  259. // Confirmation Modal
  260. <View className="flex flex-col rounded-2xl bg-white overflow-hidden w-[80%]">
  261. <View className="bg-[#E3F2F8]">
  262. <Text className="text-base lg:text-lg font-[500] text-center p-4">
  263. 確認車牌號碼
  264. </Text>
  265. </View>
  266. <View className="p-4">
  267. <Text className="text-sm lg:text-base font-[500] text-center mb-4">
  268. 您輸入的車牌號碼為:{licensePlate}
  269. </Text>
  270. </View>
  271. <View className="flex-row p-4 space-x-4">
  272. <View className="flex-1">
  273. <NormalButton
  274. title={<Text className="text-white text-sm lg:text-lg">取消</Text>}
  275. onPress={() => setShowConfirmationModal(false)}
  276. />
  277. </View>
  278. <View className="flex-1">
  279. <NormalButton
  280. title={<Text className="text-white text-sm lg:text-lg">確認</Text>}
  281. onPress={() => {
  282. saveLicensePlate(licensePlate);
  283. setShowConfirmationModal(false);
  284. setShowLicencePlateMessage(false);
  285. setLicensePlate('');
  286. }}
  287. />
  288. </View>
  289. </View>
  290. </View>
  291. )}
  292. </View>
  293. </Modal>
  294. )}
  295. <ScrollView showsVerticalScrollIndicator={false} className="flex-1 mx-[5%] ">
  296. <View className=" flex-1 pt-8 ">
  297. <View className="flex-row items-center pb-4">
  298. <HomeIconSvg />
  299. <View className="pl-2 flex-1 flex-column ">
  300. <View className="flex-row justify-between mr-[10%]">
  301. <Text className="text-lg text-left pb-1">你好!</Text>
  302. <View className="relative z-5">
  303. <Pressable
  304. onPress={() => router.push({ pathname: 'notificationPage' })}
  305. disabled={isLoadingReservations}
  306. className="z-10 w-10 items-center justify-center"
  307. hitSlop={{ top: 20, bottom: 20, left: 20, right: 20 }}
  308. >
  309. <View className="w-6 h-6">
  310. <BellIconSvg />
  311. </View>
  312. {unreadCount > 0 && (
  313. <View className="absolute -top-2 -right-[0.5] bg-red-500 rounded-full w-5 h-5 items-center justify-center">
  314. <Text className="text-white text-xs font-bold">{unreadCount}</Text>
  315. </View>
  316. )}
  317. </Pressable>
  318. <Pressable className="z-10 top-9 right-0" onPress={() => handleGoWhatsApp()}>
  319. <View className="w-8 h-8">
  320. <WhatsAppSvg />
  321. </View>
  322. </Pressable>
  323. </View>
  324. </View>
  325. <Text className="text-4xl font-light ">{user?.nickname}</Text>
  326. </View>
  327. </View>
  328. <View className=" flex-1 justify-center ">
  329. {/* <Pressable onPress={() => router.push('searchPage')}>
  330. <View
  331. style={{
  332. borderWidth: 1,
  333. padding: 24,
  334. borderRadius: 12,
  335. borderColor: '#bbbbbb',
  336. maxWidth: '100%'
  337. }}
  338. >
  339. <Text style={{ color: '#888888', fontSize: 16 }}>搜尋充電站或地區..</Text>
  340. </View>
  341. </Pressable> */}
  342. </View>
  343. </View>
  344. <View className="flex-1">
  345. <View className="my-4">
  346. <NormalButton
  347. onPress={() => router.push('scanQrPage')}
  348. // onPress={() => router.push('optionPage')}
  349. title={
  350. <View className="flex flex-row justify-start">
  351. <QrCodeIconSvg />
  352. <Text className="text-white font-bold text-lg ml-2">掃描及充電</Text>
  353. </View>
  354. }
  355. extendedStyle={{
  356. alignItems: 'flex-start',
  357. padding: 24
  358. }}
  359. />
  360. </View>
  361. <View className="flex-1 flex-row justify-between gap-6">
  362. {/* <View className="flex-1">
  363. <NormalButton
  364. // onPress={() => router.push('bookingMenuPage')}
  365. onPress={() => Alert.alert('即將推出', '此功能即將推出,敬請期待!')}
  366. //onPress={() => notificationStorage.clearStorage()}
  367. title={
  368. <View className="flex flex-row space-x-2 items-center ">
  369. <MyBookingIconSvg />
  370. <Text className="text-white font-bold text-lg ml-2">我的預約</Text>
  371. </View>
  372. }
  373. extendedStyle={{
  374. alignItems: 'flex-start',
  375. padding: 24
  376. }}
  377. />
  378. </View> */}
  379. <View className="flex-1">
  380. <NormalButton
  381. onPress={() => router.push('/(account)/(wallet)/walletPage')}
  382. title={
  383. <View className="flex flex-row space-x-2 items-center">
  384. <MyWalletSvg />
  385. <Text className="text-white font-bold text-lg ml-2">錢包</Text>
  386. </View>
  387. }
  388. extendedStyle={{
  389. alignItems: 'flex-start',
  390. padding: 24
  391. }}
  392. />
  393. </View>
  394. </View>
  395. <View className="mt-4">
  396. <NormalButton
  397. // onPress={() => console.log('掃瞄及充電')}
  398. onPress={() => router.push('vipQrPage')}
  399. title={
  400. <View className="flex flex-row items-center space-x-2">
  401. <VipCodeIconSvg />
  402. <Text className="text-white font-bold text-lg ml-2">專屬會員二維碼</Text>
  403. </View>
  404. }
  405. extendedStyle={{
  406. alignItems: 'flex-start',
  407. padding: 24
  408. }}
  409. />
  410. </View>
  411. </View>
  412. </ScrollView>
  413. </SafeAreaView>
  414. );
  415. };
  416. export default HomePage;