homePage.tsx 26 KB

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