homePage.tsx 26 KB

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