homePage.tsx 25 KB

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