diff --git a/app/(application)/announcement/index.tsx b/app/(application)/announcement/index.tsx index 83cedb4..c62c9d7 100644 --- a/app/(application)/announcement/index.tsx +++ b/app/(application)/announcement/index.tsx @@ -2,14 +2,14 @@ import BorderBottomItem from "@/components/borderBottomItem"; import InputSearch from "@/components/inputSearch"; import SkeletonContent from "@/components/skeletonContent"; import Text from '@/components/Text'; -import { ColorsStatus } from "@/constants/ColorsStatus"; import Styles from "@/constants/Styles"; import { apiGetAnnouncement } from "@/lib/api"; import { useAuthSession } from "@/providers/AuthProvider"; import { useTheme } from "@/providers/ThemeProvider"; import { MaterialIcons } from "@expo/vector-icons"; +import { useInfiniteQuery } from "@tanstack/react-query"; import { router } from "expo-router"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { RefreshControl, View, VirtualizedList } from "react-native"; import { useSelector } from "react-redux"; @@ -20,68 +20,60 @@ type Props = { createdAt: string } - export default function Announcement() { const { token, decryptToken } = useAuthSession() const { colors } = useTheme(); - const [data, setData] = useState([]) const [search, setSearch] = useState('') const update = useSelector((state: any) => state.announcementUpdate) - const [loading, setLoading] = useState(true) const arrSkeleton = Array.from({ length: 5 }, (_, index) => index) - const [page, setPage] = useState(1) - const [waiting, setWaiting] = useState(false) - const [refreshing, setRefreshing] = useState(false) - async function handleLoad(loading: boolean, thisPage: number) { - try { - setWaiting(true) - setLoading(loading) - setPage(thisPage) + // TanStack Query Infinite Query + const { + data, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isLoading, + refetch, + isRefetching + } = useInfiniteQuery({ + queryKey: ['announcements', search], + queryFn: async ({ pageParam = 1 }) => { const hasil = await decryptToken(String(token?.current)) - const response = await apiGetAnnouncement({ user: hasil, search: search, page: thisPage }) - if (thisPage == 1) { - setData(response.data) - } else if (thisPage > 1 && response.data.length > 0) { - setData([...data, ...response.data]) - } else { - return; - } - } catch (error) { - console.error(error) - } finally { - setLoading(false) - setWaiting(false) - } - } + const response = await apiGetAnnouncement({ + user: hasil, + search: search, + page: pageParam + }) + return response.data + }, + initialPageParam: 1, + getNextPageParam: (lastPage, allPages) => { + return lastPage.length > 0 ? allPages.length + 1 : undefined + }, + }) + // Trigger refetch when Redux state 'update' changes useEffect(() => { - handleLoad(false, 1) - }, [update]) + refetch() + }, [update, refetch]) - useEffect(() => { - handleLoad(true, 1) - }, [search]) + // Flatten data from pages + const flattenedData = useMemo(() => { + return data?.pages.flat() || [] + }, [data]) const loadMoreData = () => { - if (waiting) return - setTimeout(() => { - handleLoad(false, page + 1) - }, 1000); - }; - - const handleRefresh = async () => { - setRefreshing(true) - handleLoad(false, 1) - await new Promise(resolve => setTimeout(resolve, 2000)); - setRefreshing(false) + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage() + } }; const getItem = (_data: unknown, index: number): Props => ({ - id: data[index].id, - title: data[index].title, - desc: data[index].desc, - createdAt: data[index].createdAt, + id: flattenedData[index].id, + title: flattenedData[index].title, + desc: flattenedData[index].desc, + createdAt: flattenedData[index].createdAt, }) return ( @@ -91,18 +83,18 @@ export default function Announcement() { { - loading ? + isLoading && !flattenedData.length ? arrSkeleton.map((item, index) => { return ( ) }) : - data.length > 0 + flattenedData.length > 0 ? data.length} + data={flattenedData} + getItemCount={() => flattenedData.length} getItem={getItem} renderItem={({ item, index }: { item: Props, index: number }) => { return ( @@ -112,9 +104,7 @@ export default function Announcement() { borderType="bottom" bgColor="transparent" icon={ - // - // } title={item.title} desc={item.desc.replace(/<[^>]*>?/gm, '').replace(/\r?\n|\r/g, ' ')} @@ -122,14 +112,14 @@ export default function Announcement() { /> ) }} - keyExtractor={(item, index) => String(index)} + keyExtractor={(item, index) => String(item.id || index)} onEndReached={loadMoreData} onEndReachedThreshold={0.5} showsVerticalScrollIndicator={false} refreshControl={ } diff --git a/app/(application)/banner/index.tsx b/app/(application)/banner/index.tsx index dc6ce62..c5560ab 100644 --- a/app/(application)/banner/index.tsx +++ b/app/(application)/banner/index.tsx @@ -1,3 +1,4 @@ +import styles from "@/components/AppHeader" import AppHeader from "@/components/AppHeader" import HeaderRightBannerList from "@/components/banner/headerBannerList" import BorderBottomItem from "@/components/borderBottomItem" @@ -5,6 +6,7 @@ import DrawerBottom from "@/components/drawerBottom" import MenuItemRow from "@/components/menuItemRow" import ModalConfirmation from "@/components/ModalConfirmation" import ModalLoading from "@/components/modalLoading" +import Skeleton from "@/components/skeleton" import Text from "@/components/Text" import { ConstEnv } from "@/constants/ConstEnv" import Styles from "@/constants/Styles" @@ -13,11 +15,12 @@ import { setEntities } from "@/lib/bannerSlice" import { useAuthSession } from "@/providers/AuthProvider" import { useTheme } from "@/providers/ThemeProvider" import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons" +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query" import * as FileSystem from 'expo-file-system' import { startActivityAsync } from 'expo-intent-launcher' import { router, Stack } from "expo-router" import * as Sharing from 'expo-sharing' -import { useState } from "react" +import { useEffect, useState } from "react" import { Alert, Image, Platform, RefreshControl, SafeAreaView, ScrollView, View } from "react-native" import ImageViewing from 'react-native-image-viewing' import * as mime from 'react-native-mime-types' @@ -43,36 +46,51 @@ export default function BannerList() { const [loadingOpen, setLoadingOpen] = useState(false) const [viewImg, setViewImg] = useState(false) const [showDeleteModal, setShowDeleteModal] = useState(false) + const queryClient = useQueryClient() - const handleDeleteEntity = async () => { - try { - const hasil = await decryptToken(String(token?.current)); - const deletedEntity = await apiDeleteBanner({ user: hasil }, dataId); - if (deletedEntity.success) { - Toast.show({ type: 'small', text1: 'Berhasil menghapus data', }) - apiGetBanner({ user: hasil }).then((data) => - dispatch(setEntities(data.data)) - ); - } else { - Toast.show({ type: 'small', text1: 'Gagal menghapus data', }) - } - } catch (error: any) { - console.error(error); - const message = error?.response?.data?.message || "Gagal menghapus data" + // 1. Fetching logic with useQuery + const { data: bannersRes, isLoading } = useQuery({ + queryKey: ['banners'], + queryFn: async () => { + const hasil = await decryptToken(String(token?.current)) + const response = await apiGetBanner({ user: hasil }) + return response.data || [] + }, + enabled: !!token?.current, + staleTime: 0, + }) - Toast.show({ type: 'small', text1: message }) - } finally { - setModal(false) + // Sync results with Redux + useEffect(() => { + if (bannersRes) { + dispatch(setEntities(bannersRes)) } + }, [bannersRes, dispatch]) + + // 2. Deletion logic with useMutation + const deleteMutation = useMutation({ + mutationFn: async (id: string) => { + const hasil = await decryptToken(String(token?.current)) + return await apiDeleteBanner({ user: hasil }, id) + }, + onSuccess: () => { + Toast.show({ type: 'small', text1: 'Berhasil menghapus data' }) + queryClient.invalidateQueries({ queryKey: ['banners'] }) + }, + onError: (error: any) => { + const message = error?.response?.data?.message || "Gagal menghapus data" + Toast.show({ type: 'small', text1: message }) + } + }) + + const handleDeleteEntity = () => { + deleteMutation.mutate(dataId) + setModal(false) }; const handleRefresh = async () => { setRefreshing(true) - const hasil = await decryptToken(String(token?.current)); - apiGetBanner({ user: hasil }).then((data) => - dispatch(setEntities(data.data)) - ); - await new Promise(resolve => setTimeout(resolve, 2000)); + await queryClient.invalidateQueries({ queryKey: ['banners'] }) setRefreshing(false) }; @@ -140,36 +158,40 @@ export default function BannerList() { } style={[Styles.h100, { backgroundColor: colors.background }]} > - { - entities.length > 0 - ? - - {entities.map((index: any, key: number) => ( - { - setDataId(index.id) - setSelectFile(index) - setModal(true) - }} - borderType="all" - icon={ - - } - title={index.title} - /> - ))} - - : - - Tidak ada data - - } - - + + { + isLoading ? ( + <> + + + + + ) : + entities.length > 0 ? + entities.map((index: any, key: number) => ( + { + setDataId(index.id) + setSelectFile(index) + setModal(true) + }} + borderType="all" + icon={ + + } + title={index.title} + /> + )) + : + + Tidak ada data + + } + setModal(false)} title="Menu"> diff --git a/app/(application)/discussion/index.tsx b/app/(application)/discussion/index.tsx index 4dbee12..14b521d 100644 --- a/app/(application)/discussion/index.tsx +++ b/app/(application)/discussion/index.tsx @@ -11,8 +11,9 @@ import { apiGetDiscussionGeneral } from "@/lib/api"; import { useAuthSession } from "@/providers/AuthProvider"; import { useTheme } from "@/providers/ThemeProvider"; import { AntDesign, Feather, Ionicons, MaterialIcons } from "@expo/vector-icons"; +import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; import { router, useLocalSearchParams } from "expo-router"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { RefreshControl, View, VirtualizedList } from "react-native"; import { useSelector } from "react-redux"; @@ -32,70 +33,76 @@ export default function Discussion() { const { colors } = useTheme(); const { active, group } = useLocalSearchParams<{ active?: string, group?: string }>() const [search, setSearch] = useState('') - const [nameGroup, setNameGroup] = useState('') - const [data, setData] = useState([]) const update = useSelector((state: any) => state.discussionGeneralDetailUpdate) - const [loading, setLoading] = useState(true) - const arrSkeleton = Array.from({ length: 5 }, (_, index) => index) - const [status, setStatus] = useState<'true' | 'false'>('true') - const [page, setPage] = useState(1) - const [waiting, setWaiting] = useState(false) + const queryClient = useQueryClient() + const [status, setStatus] = useState<'true' | 'false'>(active == 'false' ? 'false' : 'true') const [refreshing, setRefreshing] = useState(false) - async function handleLoad(loading: boolean, thisPage: number) { - try { - setWaiting(true) - setLoading(loading) - setPage(thisPage) + // TanStack Query for Discussions with Infinite Scroll + const { + data, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isLoading, + refetch + } = useInfiniteQuery({ + queryKey: ['discussions', { status, search, group }], + queryFn: async ({ pageParam = 1 }) => { const hasil = await decryptToken(String(token?.current)) - const response = await apiGetDiscussionGeneral({ user: hasil, active: status, search: search, group: String(group), page: thisPage }) - if (thisPage == 1) { - setData(response.data) - } else if (thisPage > 1 && response.data.length > 0) { - setData([...data, ...response.data]) - } else { - return; - } - setNameGroup(response.filter.name) - } catch (error) { - console.error(error) - } finally { - setLoading(false) - setWaiting(false) - } - } + const response = await apiGetDiscussionGeneral({ + user: hasil, + active: status, + search: search, + group: String(group), + page: pageParam + }) + return response; + }, + initialPageParam: 1, + getNextPageParam: (lastPage, allPages) => { + return lastPage.data.length > 0 ? allPages.length + 1 : undefined; + }, + enabled: !!token?.current, + staleTime: 0, + }) + // Flatten pages into a single data array + const flatData = useMemo(() => { + return data?.pages.flatMap(page => page.data) || []; + }, [data]) + // Get nameGroup from the first available page + const nameGroup = useMemo(() => { + return data?.pages[0]?.filter?.name || ""; + }, [data]) + + // Refetch when manual update state changes useEffect(() => { - handleLoad(false, 1) - }, [update]) - - useEffect(() => { - handleLoad(true, 1) - }, [status, search, group]) - - - const loadMoreData = () => { - if (waiting) return - setTimeout(() => { - handleLoad(false, page + 1) - }, 1000); - }; + refetch() + }, [update, refetch]) const handleRefresh = async () => { setRefreshing(true) - handleLoad(false, 1) - await new Promise(resolve => setTimeout(resolve, 2000)); + await queryClient.invalidateQueries({ queryKey: ['discussions'] }) setRefreshing(false) }; + const loadMoreData = () => { + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage() + } + }; + + const arrSkeleton = [0, 1, 2, 3, 4] + const getItem = (_data: unknown, index: number): Props => ({ - id: data[index].id, - title: data[index].title, - desc: data[index].desc, - status: data[index].status, - total_komentar: data[index].total_komentar, - createdAt: data[index].createdAt, + id: flatData[index]?.id, + title: flatData[index]?.title, + desc: flatData[index]?.desc, + status: flatData[index]?.status, + total_komentar: flatData[index]?.total_komentar, + createdAt: flatData[index]?.createdAt, }) return ( @@ -132,18 +139,18 @@ export default function Discussion() { { - loading ? + isLoading ? arrSkeleton.map((item: any, i: number) => { return ( ) }) : - data.length > 0 + flatData.length > 0 ? data.length} + data={flatData} + getItemCount={() => flatData.length} getItem={getItem} renderItem={({ item, index }: { item: Props, index: number }) => { return ( @@ -153,16 +160,14 @@ export default function Discussion() { onPress={() => { router.push(`/discussion/${item.id}`) }} borderType="bottom" icon={ - // - // } title={item.title} subtitle={ status != "false" && } rightTopInfo={item.createdAt} - desc={item.desc.replace(/<[^>]*>?/gm, ' ').replace(/\r?\n|\r/g, ' ')} + desc={item.desc?.replace(/<[^>]*>?/gm, ' ').replace(/\r?\n|\r/g, ' ')} leftBottomInfo={ diff --git a/app/(application)/division/index.tsx b/app/(application)/division/index.tsx index 5537d86..40bbc82 100644 --- a/app/(application)/division/index.tsx +++ b/app/(application)/division/index.tsx @@ -17,8 +17,9 @@ import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons"; +import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; import { router, useLocalSearchParams } from "expo-router"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Pressable, RefreshControl, View, VirtualizedList } from "react-native"; import { useSelector } from "react-redux"; @@ -40,23 +41,23 @@ export default function ListDivision() { const { token, decryptToken } = useAuthSession() const { colors } = useTheme(); const [search, setSearch] = useState("") - const [nameGroup, setNameGroup] = useState("") - const [data, setData] = useState([]) - // ... state same ... + const queryClient = useQueryClient() + const [status, setStatus] = useState<'true' | 'false'>(active == 'false' ? 'false' : 'true') + const [category, setCategory] = useState<'divisi-saya' | 'semua'>(cat == 'semua' ? 'semua' : 'divisi-saya') const update = useSelector((state: any) => state.divisionUpdate) - const arrSkeleton = Array.from({ length: 3 }, (_, index) => index) - const [loading, setLoading] = useState(false) - const [status, setStatus] = useState<'true' | 'false'>('true') - const [category, setCategory] = useState<'divisi-saya' | 'semua'>('divisi-saya') - const [page, setPage] = useState(1) - const [waiting, setWaiting] = useState(false) const [refreshing, setRefreshing] = useState(false) - async function handleLoad(loading: boolean, thisPage: number) { - try { - setWaiting(true) - setLoading(loading) - setPage(thisPage) + // TanStack Query for Divisions with Infinite Scroll + const { + data, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isLoading, + refetch + } = useInfiniteQuery({ + queryKey: ['divisions', { status, search, group, category }], + queryFn: async ({ pageParam = 1 }) => { const hasil = await decryptToken(String(token?.current)); const response = await apiGetDivision({ user: hasil, @@ -64,54 +65,52 @@ export default function ListDivision() { search: search, group: String(group), kategori: category, - page: thisPage + page: pageParam }); + return response; + }, + initialPageParam: 1, + getNextPageParam: (lastPage, allPages) => { + return lastPage.data.length > 0 ? allPages.length + 1 : undefined; + }, + enabled: !!token?.current, + staleTime: 0, + }) - if (response.success) { - if (thisPage == 1) { - setData(response.data); - } else if (thisPage > 1 && response.data.length > 0) { - setData([...data, ...response.data]); - } else { - return; - } - setNameGroup(response.filter.name); - } - } catch (error) { - console.error(error); - } finally { - setLoading(false) - setWaiting(false) - } - } - + // Refetch when manual update state changes useEffect(() => { - handleLoad(false, 1); - }, [update]); + refetch() + }, [update, refetch]) - useEffect(() => { - handleLoad(true, 1); - }, [status, search, group, category]); + // Flatten pages into a single data array + const flatData = useMemo(() => { + return data?.pages.flatMap(page => page.data) || []; + }, [data]) - const loadMoreData = () => { - if (waiting) return - setTimeout(() => { - handleLoad(false, page + 1) - }, 1000); - }; + // Get nameGroup from the first available page + const nameGroup = useMemo(() => { + return data?.pages[0]?.filter?.name || ""; + }, [data]) const handleRefresh = async () => { setRefreshing(true) - handleLoad(false, 1) - await new Promise(resolve => setTimeout(resolve, 2000)); + await queryClient.invalidateQueries({ queryKey: ['divisions'] }) setRefreshing(false) }; + const loadMoreData = () => { + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage() + } + }; + + const arrSkeleton = [0, 1, 2] + const getItem = (_data: unknown, index: number): Props => ({ - id: data[index].id, - name: data[index].name, - desc: data[index].desc, - jumlah_member: data[index].jumlah_member, + id: flatData[index]?.id, + name: flatData[index]?.name, + desc: flatData[index]?.desc, + jumlah_member: flatData[index]?.jumlah_member, }) @@ -206,7 +205,7 @@ export default function ListDivision() { { - loading ? + isLoading ? isList ? arrSkeleton.map((item, index) => ( @@ -216,7 +215,7 @@ export default function ListDivision() { )) : - data.length == 0 ? ( + flatData.length == 0 ? ( Tidak ada data @@ -224,9 +223,9 @@ export default function ListDivision() { isList ? ( data.length} + getItemCount={() => flatData.length} getItem={getItem} renderItem={({ item, index }: { item: Props, index: number }) => { return ( @@ -260,9 +259,9 @@ export default function ListDivision() { ) : ( data.length} + getItemCount={() => flatData.length} getItem={getItem} renderItem={({ item, index }: { item: Props, index: number }) => { return ( diff --git a/app/(application)/feature.tsx b/app/(application)/feature.tsx index 00b2311..23cfba6 100644 --- a/app/(application)/feature.tsx +++ b/app/(application)/feature.tsx @@ -11,6 +11,8 @@ export default function Feature() { const entityUser = useSelector((state: any) => state.user) const { colors } = useTheme(); + console.log({entityUser}) + return ( ([]) const [search, setSearch] = useState('') - const arrSkeleton = Array.from({ length: 5 }, (_, index) => index) - const [loading, setLoading] = useState(true) const [showDeleteModal, setShowDeleteModal] = useState(false) const [status, setStatus] = useState<'true' | 'false'>('true') const [loadingSubmit, setLoadingSubmit] = useState(false) const [idChoose, setIdChoose] = useState('') const [activeChoose, setActiveChoose] = useState(true) const [titleChoose, setTitleChoose] = useState('') + const queryClient = useQueryClient() const [refreshing, setRefreshing] = useState(false) const dispatch = useDispatch() @@ -49,12 +48,38 @@ export default function Index() { title: false, }); + // TanStack Query for Groups + const { + data: queryData, + isLoading, + refetch + } = useQuery({ + queryKey: ['groups', { status, search }], + queryFn: async () => { + const hasil = await decryptToken(String(token?.current)) + const response = await apiGetGroup({ + user: hasil, + active: status, + search: search + }) + return response; + }, + enabled: !!token?.current, + staleTime: 0, + }) + + const data = useMemo(() => queryData?.data || [], [queryData]) + + useEffect(() => { + refetch() + }, [update, refetch]) async function handleEdit() { try { setLoadingSubmit(true) const hasil = await decryptToken(String(token?.current)) const response = await apiEditGroup({ user: hasil, name: titleChoose }, idChoose) + await queryClient.invalidateQueries({ queryKey: ['groups'] }) dispatch(setUpdateGroup(!update)) } catch (error) { console.error(error) @@ -71,6 +96,7 @@ export default function Index() { try { const hasil = await decryptToken(String(token?.current)) const response = await apiDeleteGroup({ user: hasil, isActive: activeChoose }, idChoose) + await queryClient.invalidateQueries({ queryKey: ['groups'] }) dispatch(setUpdateGroup(!update)) } catch (error) { console.error(error) @@ -80,32 +106,9 @@ export default function Index() { } } - async function handleLoad(loading: boolean) { - try { - setLoading(loading) - const hasil = await decryptToken(String(token?.current)) - const response = await apiGetGroup({ user: hasil, active: status, search: search }) - setData(response.data) - } catch (error) { - console.error(error) - } finally { - setLoading(false) - - } - } - - useEffect(() => { - handleLoad(false) - }, [update]) - - useEffect(() => { - handleLoad(true) - }, [status, search]) - const handleRefresh = async () => { setRefreshing(true) - handleLoad(false) - await new Promise(resolve => setTimeout(resolve, 2000)); + await queryClient.invalidateQueries({ queryKey: ['groups'] }) setRefreshing(false) }; @@ -129,6 +132,8 @@ export default function Index() { + const arrSkeleton = [0, 1, 2, 3, 4] + return ( @@ -152,7 +157,7 @@ export default function Index() { { - loading ? + isLoading ? arrSkeleton.map((item, index) => { return ( diff --git a/app/(application)/home.tsx b/app/(application)/home.tsx index 2c8e1d9..b45163c 100644 --- a/app/(application)/home.tsx +++ b/app/(application)/home.tsx @@ -12,6 +12,7 @@ import { apiGetProfile } from "@/lib/api"; import { setEntities } from "@/lib/entitiesSlice"; import { useAuthSession } from "@/providers/AuthProvider"; import { useTheme } from "@/providers/ThemeProvider"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { LinearGradient } from "expo-linear-gradient"; import { Stack } from "expo-router"; import { useEffect, useState } from "react"; @@ -23,28 +24,46 @@ import { useDispatch, useSelector } from "react-redux"; export default function Home() { const entities = useSelector((state: any) => state.entities) const dispatch = useDispatch() + const queryClient = useQueryClient() const { token, decryptToken, signOut } = useAuthSession() const { colors } = useTheme(); const insets = useSafeAreaInsets() const [refreshing, setRefreshing] = useState(false) - useEffect(() => { - handleUserLogin() - }, [dispatch]); + const { data: profile, isError } = useQuery({ + queryKey: ['profile'], + queryFn: async () => { + const hasil = await decryptToken(String(token?.current)) + const data = await apiGetProfile({ id: hasil }) + return data.data + }, + enabled: !!token?.current, + staleTime: 0, // Ensure it refetches every time the component mounts + }) - async function handleUserLogin() { - const hasil = await decryptToken(String(token?.current)) - apiGetProfile({ id: hasil }) - .then((data) => dispatch(setEntities(data.data))) - .catch((error) => { - signOut() - }); - } + // Sync to Redux for global usage + useEffect(() => { + if (profile) { + dispatch(setEntities(profile)) + } + }, [profile, dispatch]) + + // Auto Sign Out if profile fetch fails (e.g. invalid/expired token) + useEffect(() => { + if (isError) { + signOut() + } + }, [isError, signOut]) const handleRefresh = async () => { setRefreshing(true) - handleUserLogin() - await new Promise(resolve => setTimeout(resolve, 2000)); + // Invalidate all queries related to the home screen + await queryClient.invalidateQueries({ queryKey: ['profile'] }) + await queryClient.invalidateQueries({ queryKey: ['banners'] }) + await queryClient.invalidateQueries({ queryKey: ['homeData'] }) + + // Artificial delay to show refresh indicator if sync is too fast + await new Promise(resolve => setTimeout(resolve, 1000)); setRefreshing(false) }; diff --git a/app/(application)/member/index.tsx b/app/(application)/member/index.tsx index cab1d5c..1a9db89 100644 --- a/app/(application)/member/index.tsx +++ b/app/(application)/member/index.tsx @@ -12,8 +12,9 @@ import { apiGetUser } from "@/lib/api"; import { useAuthSession } from "@/providers/AuthProvider"; import { useTheme } from "@/providers/ThemeProvider"; import { AntDesign, Feather } from "@expo/vector-icons"; +import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; import { router, useLocalSearchParams } from "expo-router"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { RefreshControl, View, VirtualizedList } from "react-native"; import { useSelector } from "react-redux"; @@ -37,73 +38,81 @@ export default function Index() { const entityUser = useSelector((state: any) => state.user) const { colors } = useTheme(); const [search, setSearch] = useState('') - const [nameGroup, setNameGroup] = useState('') - const [data, setData] = useState([]) const update = useSelector((state: any) => state.memberUpdate) - const arrSkeleton = Array.from({ length: 5 }, (_, index) => index) - const [loading, setLoading] = useState(true) - const [status, setStatus] = useState<'true' | 'false'>('true') - const [page, setPage] = useState(1) - const [waiting, setWaiting] = useState(false) + const queryClient = useQueryClient() + const [status, setStatus] = useState<'true' | 'false'>(active == 'false' ? 'false' : 'true') const [refreshing, setRefreshing] = useState(false) - async function handleLoad(loading: boolean, thisPage: number) { - try { - setWaiting(true) - setLoading(loading) - setPage(thisPage) + // TanStack Query for Members with Infinite Scroll + const { + data, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isLoading, + refetch + } = useInfiniteQuery({ + queryKey: ['members', { status, search, group }], + queryFn: async ({ pageParam = 1 }) => { const hasil = await decryptToken(String(token?.current)) - const response = await apiGetUser({ user: hasil, active: status, search, group: String(group), page: thisPage }) - if (thisPage == 1) { - setData(response.data) - } else if (thisPage > 1 && response.data.length > 0) { - setData([...data, ...response.data]) - } else { - return; - } - setNameGroup(response.filter.name) - } catch (error) { - console.error(error) - } finally { - setLoading(false) - setWaiting(false) - } - } + const response = await apiGetUser({ + user: hasil, + active: status, + search, + group: String(group), + page: pageParam + }) + return response; + }, + initialPageParam: 1, + getNextPageParam: (lastPage, allPages) => { + return lastPage.data.length > 0 ? allPages.length + 1 : undefined; + }, + enabled: !!token?.current, + staleTime: 0, + }) - const loadMoreData = () => { - if (waiting) return - setTimeout(() => { - handleLoad(false, page + 1) - }, 1000); - }; + // Flatten pages into a single data array + const flatData = useMemo(() => { + return data?.pages.flatMap(page => page.data) || []; + }, [data]) + // Get nameGroup from the first available page + const nameGroup = useMemo(() => { + return data?.pages[0]?.filter?.name || ""; + }, [data]) + + // Refetch when manual update state changes useEffect(() => { - handleLoad(false, 1) - }, [update]) - - useEffect(() => { - handleLoad(true, 1) - }, [group, search, status]) + refetch() + }, [update, refetch]) const handleRefresh = async () => { setRefreshing(true) - handleLoad(false, 1) - await new Promise(resolve => setTimeout(resolve, 2000)); + await queryClient.invalidateQueries({ queryKey: ['members'] }) setRefreshing(false) }; + const loadMoreData = () => { + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage() + } + }; + + const arrSkeleton = [0, 1, 2, 3, 4] + const getItem = (_data: unknown, index: number): Props => ({ - id: data[index].id, - name: data[index].name, - nik: data[index].nik, - email: data[index].email, - phone: data[index].phone, - gender: data[index].gender, - position: data[index].position, - group: data[index].group, - img: data[index].img, - isActive: data[index].isActive, - role: data[index].role, + id: flatData[index]?.id, + name: flatData[index]?.name, + nik: flatData[index]?.nik, + email: flatData[index]?.email, + phone: flatData[index]?.phone, + gender: flatData[index]?.gender, + position: flatData[index]?.position, + group: flatData[index]?.group, + img: flatData[index]?.img, + isActive: flatData[index]?.isActive, + role: flatData[index]?.role, }); return ( @@ -136,18 +145,18 @@ export default function Index() { { - loading ? + isLoading ? arrSkeleton.map((item, index) => { return ( ) }) : - data.length > 0 + flatData.length > 0 ? data.length} + data={flatData} + getItemCount={() => flatData.length} getItem={getItem} renderItem={({ item, index }: { item: Props, index: number }) => { return ( diff --git a/app/(application)/notification.tsx b/app/(application)/notification.tsx index 0ef9ea7..c985a6f 100644 --- a/app/(application)/notification.tsx +++ b/app/(application)/notification.tsx @@ -1,4 +1,3 @@ -import BorderBottomItem from "@/components/borderBottomItem"; import BorderBottomItemVertical from "@/components/borderBottomItemVertical"; import SkeletonTwoItem from "@/components/skeletonTwoItem"; import Text from "@/components/Text"; @@ -10,7 +9,8 @@ import { pushToPage } from "@/lib/pushToPage"; import { useAuthSession } from "@/providers/AuthProvider"; import { useTheme } from "@/providers/ThemeProvider"; import { Feather } from "@expo/vector-icons"; -import { useEffect, useState } from "react"; +import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; +import { useEffect, useMemo, useState } from "react"; import { RefreshControl, SafeAreaView, View, VirtualizedList } from "react-native"; import { useDispatch, useSelector } from "react-redux"; @@ -27,64 +27,61 @@ type Props = { export default function Notification() { const { token, decryptToken } = useAuthSession() const { colors } = useTheme(); - const [loading, setLoading] = useState(false) - const [data, setData] = useState([]) - const [page, setPage] = useState(1) - const [waiting, setWaiting] = useState(false) - const arrSkeleton = Array.from({ length: 5 }, (_, index) => index) + const queryClient = useQueryClient() const dispatch = useDispatch() const updateNotification = useSelector((state: any) => state.notificationUpdate) const [refreshing, setRefreshing] = useState(false) - async function handleLoad(loading: boolean, thisPage: number) { - try { - setLoading(loading) - setPage(thisPage) - setWaiting(true) + // TanStack Query for Notifications with Infinite Scroll + const { + data, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isLoading, + refetch + } = useInfiniteQuery({ + queryKey: ['notifications'], + queryFn: async ({ pageParam = 1 }) => { const hasil = await decryptToken(String(token?.current)) - const response = await apiGetNotification({ user: hasil, page: thisPage }) - if (thisPage == 1) { - setData(response.data) - } else if (thisPage > 1 && response.data.length > 0) { - setData([...data, ...response.data]) - } else { - return; - } - } catch (error) { - console.error(error) - } finally { - setLoading(false) - setWaiting(false) - } - } + const response = await apiGetNotification({ user: hasil, page: pageParam }) + return response; + }, + initialPageParam: 1, + getNextPageParam: (lastPage, allPages) => { + return lastPage.data.length > 0 ? allPages.length + 1 : undefined; + }, + enabled: !!token?.current, + staleTime: 0, + }) + // Flatten pages into a single data array + const flatData = useMemo(() => { + return data?.pages.flatMap(page => page.data) || []; + }, [data]) - const loadMoreData = () => { - if (waiting) return - setTimeout(() => { - handleLoad(false, page + 1) - }, 1000); + // Refetch when manual update state changes + useEffect(() => { + refetch() + }, [updateNotification, refetch]) + + const handleRefresh = async () => { + setRefreshing(true) + await queryClient.invalidateQueries({ queryKey: ['notifications'] }) + setRefreshing(false) }; - useEffect(() => { - handleLoad(true, 1) - }, []) - - - const getItem = (_data: unknown, index: number): Props => ({ - id: data[index].id, - title: data[index].title, - desc: data[index].desc, - category: data[index].category, - idContent: data[index].idContent, - isRead: data[index].isRead, - createdAt: data[index].createdAt, - }); + const loadMoreData = () => { + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage() + } + }; async function handleReadNotification(id: string, category: string, idContent: string) { try { const hasil = await decryptToken(String(token?.current)) const response = await apiReadOneNotification({ user: hasil, id: id }) + await queryClient.invalidateQueries({ queryKey: ['notifications'] }) pushToPage(category, idContent) dispatch(setUpdateNotification(!updateNotification)) } catch (error) { @@ -92,28 +89,33 @@ export default function Notification() { } } - const handleRefresh = async () => { - setRefreshing(true) - handleLoad(false, 1) - await new Promise(resolve => setTimeout(resolve, 2000)); - setRefreshing(false) - }; + const arrSkeleton = [0, 1, 2, 3, 4] + + const getItem = (_data: unknown, index: number): Props => ({ + id: flatData[index]?.id, + title: flatData[index]?.title, + desc: flatData[index]?.desc, + category: flatData[index]?.category, + idContent: flatData[index]?.idContent, + isRead: flatData[index]?.isRead, + createdAt: flatData[index]?.createdAt, + }); return ( { - loading ? + isLoading ? arrSkeleton.map((item, index) => { return ( ) }) : - data.length > 0 ? + flatData.length > 0 ? data.length} + data={flatData} + getItemCount={() => flatData.length} getItem={getItem} renderItem={({ item, index }: { item: Props, index: number }) => { return ( diff --git a/app/(application)/position/index.tsx b/app/(application)/position/index.tsx index 47fe3a9..18a76b2 100644 --- a/app/(application)/position/index.tsx +++ b/app/(application)/position/index.tsx @@ -16,8 +16,9 @@ import { setUpdatePosition } from "@/lib/positionSlice"; import { useAuthSession } from "@/providers/AuthProvider"; import { useTheme } from "@/providers/ThemeProvider"; import { AntDesign, Feather, MaterialCommunityIcons } from "@expo/vector-icons"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { useLocalSearchParams } from "expo-router"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { RefreshControl, View, VirtualizedList } from "react-native"; import Toast from "react-native-toast-message"; import { useDispatch, useSelector } from "react-redux"; @@ -31,51 +32,53 @@ type Props = { } export default function Index() { - const arrSkeleton = Array.from({ length: 5 }, (_, index) => index) - const [loading, setLoading] = useState(true) const { token, decryptToken } = useAuthSession() const { colors } = useTheme() - const [status, setStatus] = useState<'true' | 'false'>('true') - const entityUser = useSelector((state: any) => state.user) const { active, group } = useLocalSearchParams<{ active?: string, group?: string }>() + const [status, setStatus] = useState<'true' | 'false'>(active == 'false' ? 'false' : 'true') + const entityUser = useSelector((state: any) => state.user) const [isModal, setModal] = useState(false) const [isVisibleEdit, setVisibleEdit] = useState(false) - const [data, setData] = useState([]) const [search, setSearch] = useState('') - const [nameGroup, setNameGroup] = useState('') const [loadingSubmit, setLoadingSubmit] = useState(false) const [chooseData, setChooseData] = useState({ name: '', id: '', active: false, idGroup: '' }) const [error, setError] = useState({ name: false, }); + const queryClient = useQueryClient() const [refreshing, setRefreshing] = useState(false) const [showDeleteModal, setShowDeleteModal] = useState(false) const dispatch = useDispatch() const update = useSelector((state: any) => state.positionUpdate) - async function handleLoad(loading: boolean) { - try { - setLoading(loading) + // TanStack Query for Positions + const { + data: queryData, + isLoading, + refetch + } = useQuery({ + queryKey: ['positions', { status, search, group }], + queryFn: async () => { const hasil = await decryptToken(String(token?.current)) - const response = await apiGetPosition({ user: hasil, active: status, search: search, group: String(group) }) - setData(response.data) - setNameGroup(response.filter.name) - } catch (error) { - console.error(error) - } finally { - setLoading(false) - } - } + const response = await apiGetPosition({ + user: hasil, + active: status, + search: search, + group: String(group) + }) + return response; + }, + enabled: !!token?.current, + staleTime: 0, + }) + + const data = useMemo(() => queryData?.data || [], [queryData]) + const nameGroup = useMemo(() => queryData?.filter?.name || "", [queryData]) useEffect(() => { - handleLoad(false) - }, [update]) - - - useEffect(() => { - handleLoad(true) - }, [status, search, group]) + refetch() + }, [update, refetch]) function handleChooseData(id: string, name: string, active: boolean, group: string) { @@ -88,7 +91,7 @@ export default function Index() { const hasil = await decryptToken(String(token?.current)) const response = await apiDeletePosition({ user: hasil, isActive: chooseData.active }, chooseData.id) dispatch(setUpdatePosition(!update)) - } catch (error : any ) { + } catch (error: any) { console.error(error); const message = error?.response?.data?.message || "Gagal menghapus data" @@ -110,7 +113,7 @@ export default function Index() { } else { Toast.show({ type: 'small', text1: response.message, }) } - } catch (error : any ) { + } catch (error: any) { console.error(error); const message = error?.response?.data?.message || "Gagal mengubah data" @@ -138,10 +141,11 @@ export default function Index() { handleEdit() } + const arrSkeleton = [0, 1, 2, 3, 4] + const handleRefresh = async () => { setRefreshing(true) - handleLoad(false) - await new Promise(resolve => setTimeout(resolve, 2000)); + await queryClient.invalidateQueries({ queryKey: ['positions'] }) setRefreshing(false) }; @@ -184,7 +188,7 @@ export default function Index() { { - loading ? + isLoading ? arrSkeleton.map((item, index) => { return ( diff --git a/app/(application)/project/index.tsx b/app/(application)/project/index.tsx index b23639e..f283d04 100644 --- a/app/(application)/project/index.tsx +++ b/app/(application)/project/index.tsx @@ -18,8 +18,9 @@ import { Ionicons, MaterialCommunityIcons, } from "@expo/vector-icons"; +import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; import { router, useLocalSearchParams } from "expo-router"; -import { useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; import { Pressable, RefreshControl, ScrollView, View, VirtualizedList } from "react-native"; import { useSelector } from "react-redux"; @@ -40,28 +41,29 @@ export default function ListProject() { cat?: string; year?: string; }>(); - const [statusFix, setStatusFix] = useState<'0' | '1' | '2' | '3'>('0') + const [statusFix, setStatusFix] = useState<'0' | '1' | '2' | '3'>( + (status == '1' || status == '2' || status == '3') ? status : '0' + ) const { token, decryptToken } = useAuthSession(); const { colors } = useTheme(); const entityUser = useSelector((state: any) => state.user) const [search, setSearch] = useState("") - const [nameGroup, setNameGroup] = useState("") - // ... state same ... - const [isYear, setYear] = useState("") - const [data, setData] = useState([]) const [isList, setList] = useState(false) const update = useSelector((state: any) => state.projectUpdate) - const [loading, setLoading] = useState(true) - const arrSkeleton = Array.from({ length: 3 }, (_, index) => index) - const [page, setPage] = useState(1) - const [waiting, setWaiting] = useState(false) + const queryClient = useQueryClient() const [refreshing, setRefreshing] = useState(false) - async function handleLoad(loading: boolean, thisPage: number) { - try { - setLoading(loading) - setWaiting(true) - setPage(thisPage) + // TanStack Query for Projects with Infinite Scroll + const { + data, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + isLoading, + refetch + } = useInfiniteQuery({ + queryKey: ['projects', { statusFix, search, group, cat, year }], + queryFn: async ({ pageParam = 1 }) => { const hasil = await decryptToken(String(token?.current)); const response = await apiGetProject({ user: hasil, @@ -69,60 +71,55 @@ export default function ListProject() { search: search, group: String(group), kategori: String(cat), - page: thisPage, + page: pageParam, year: String(year) }); + return response; + }, + initialPageParam: 1, + getNextPageParam: (lastPage, allPages) => { + return lastPage.data.length > 0 ? allPages.length + 1 : undefined; + }, + enabled: !!token?.current, + staleTime: 0, + }) - if (response.success) { - setNameGroup(response.filter.name); - setYear(response.tahun) - if (thisPage == 1) { - setData(response.data); - } else if (thisPage > 1 && response.data.length > 0) { - setData([...data, ...response.data]) - } else { - return; - } - } - } catch (error) { - console.error(error); - } finally { - setLoading(false) - setWaiting(false) - } - } - + // Refetch when manual update state changes useEffect(() => { - handleLoad(false, 1); - }, [update.data]); + refetch() + }, [update.data, refetch]) + // Flatten pages into a single data array + const flatData = useMemo(() => { + return data?.pages.flatMap(page => page.data) || []; + }, [data]) - useEffect(() => { - handleLoad(true, 1); - }, [statusFix, search, group, cat, year]); - - const loadMoreData = () => { - if (waiting) return - setTimeout(() => { - handleLoad(false, page + 1) - }, 1000); - } + // Get metadata from the first available page + const nameGroup = useMemo(() => data?.pages[0]?.filter?.name || "", [data]) + const isYear = useMemo(() => data?.pages[0]?.tahun || "", [data]) const handleRefresh = async () => { setRefreshing(true) - handleLoad(false, 1) - await new Promise(resolve => setTimeout(resolve, 2000)); + await queryClient.invalidateQueries({ queryKey: ['projects'] }) setRefreshing(false) - } + }; + + const loadMoreData = () => { + if (hasNextPage && !isFetchingNextPage) { + fetchNextPage() + } + }; + + const arrSkeleton = [0, 1, 2] const getItem = (_data: unknown, index: number): Props => ({ - id: data[index].id, - title: data[index].title, - desc: data[index].desc, - status: data[index].status, - member: data[index].member, - progress: data[index].progress, - createdAt: data[index].createdAt, + id: flatData[index]?.id, + title: flatData[index]?.title, + desc: flatData[index]?.desc, + status: flatData[index]?.status, + member: flatData[index]?.member, + progress: flatData[index]?.progress, + createdAt: flatData[index]?.createdAt, }) return ( @@ -205,7 +202,6 @@ export default function ListProject() { { - // entityUser.role != 'cosupadmin' && entityUser.role != 'admin' && Filter : { @@ -218,18 +214,13 @@ export default function ListProject() { : '' } - {/* { - (entityUser.role == 'user' || entityUser.role == 'coadmin') - ? (cat == 'null' || cat == 'undefined' || cat == undefined || cat == '' || cat == 'data-saya') ? : - : '' - } */} } { - loading ? + isLoading ? isList ? arrSkeleton.map((item, index) => ( @@ -239,13 +230,13 @@ export default function ListProject() { )) : - data.length > 0 + flatData.length > 0 ? isList ? ( data.length} + data={flatData} + getItemCount={() => flatData.length} getItem={getItem} renderItem={({ item, index }: { item: Props, index: number }) => { return ( @@ -279,35 +270,12 @@ export default function ListProject() { /> } /> - {/* { - data.map((item, index) => { - return ( - { router.push(`/project/${item.id}`); }} - borderType="bottom" - icon={ - - - - } - title={item.title} - /> - ); - }) - } */} ) : ( data.length} + data={flatData} + getItemCount={() => flatData.length} getItem={getItem} renderItem={({ item, index }: { item: Props, index: number }) => { return ( @@ -359,43 +327,6 @@ export default function ListProject() { /> } /> - {/* {data.map((item, index) => { - return ( - { - router.push(`/project/${item.id}`); - }} - content="page" - title={item.title} - headerColor="primary" - > - - - - {item.createdAt} - - - - - ); - })} */} ) : diff --git a/app/_layout.tsx b/app/_layout.tsx index 5d0acb0..cb20548 100644 --- a/app/_layout.tsx +++ b/app/_layout.tsx @@ -1,5 +1,6 @@ import AuthProvider from '@/providers/AuthProvider'; import ThemeProvider, { useTheme } from '@/providers/ThemeProvider'; +import QueryProvider from '@/providers/QueryProvider'; import { useFonts } from 'expo-font'; import { Stack } from 'expo-router'; import * as SplashScreen from 'expo-splash-screen'; @@ -48,9 +49,11 @@ export default function RootLayout() { - - - + + + + + diff --git a/bun.lock b/bun.lock index 1bf2725..2140a31 100644 --- a/bun.lock +++ b/bun.lock @@ -11,12 +11,16 @@ "@react-native-clipboard/clipboard": "^1.16.3", "@react-native-community/cli": "^19.1.0", "@react-native-community/datetimepicker": "8.4.1", + "@react-native-community/netinfo": "^12.0.1", "@react-native-firebase/app": "^22.4.0", "@react-native-firebase/database": "^22.4.0", "@react-native-firebase/messaging": "^22.2.1", "@react-navigation/bottom-tabs": "^7.2.0", "@react-navigation/native": "^7.0.14", "@reduxjs/toolkit": "^2.7.0", + "@tanstack/query-async-storage-persister": "^5.99.2", + "@tanstack/react-query": "^5.99.2", + "@tanstack/react-query-persist-client": "^5.99.2", "@types/formidable": "^3.4.5", "axios": "^1.8.4", "crypto-es": "^2.1.0", @@ -566,6 +570,8 @@ "@react-native-community/datetimepicker": ["@react-native-community/datetimepicker@8.4.1", "", { "dependencies": { "invariant": "^2.2.4" }, "peerDependencies": { "expo": ">=52.0.0", "react": "*", "react-native": "*", "react-native-windows": "*" }, "optionalPeers": ["expo", "react-native-windows"] }, "sha512-DrK+CUS5fZnz8dhzBezirkzQTcNDdaXer3oDLh0z4nc2tbdIdnzwvXCvi8IEOIvleoc9L95xS5tKUl0/Xv71Mg=="], + "@react-native-community/netinfo": ["@react-native-community/netinfo@12.0.1", "", { "peerDependencies": { "react": "*", "react-native": ">=0.59" } }, "sha512-P/3caXIvfYSJG8AWJVefukg+ZGRPs+M4Lp3pNJtgcTYoJxCjWrKQGNnCkj/Cz//zWa/avGed0i/wzm0T8vV2IQ=="], + "@react-native-firebase/app": ["@react-native-firebase/app@22.4.0", "", { "dependencies": { "firebase": "11.10.0" }, "peerDependencies": { "expo": ">=47.0.0", "react": "*", "react-native": "*" }, "optionalPeers": ["expo"] }, "sha512-mW49qYioddRZjCRiF4XMpt7pyPoh84pqU2obnFY0pWD9K0aFRv6+BfLBYrsAFY3xqA5cqf0uj+Nne0vrvmuAyw=="], "@react-native-firebase/database": ["@react-native-firebase/database@22.4.0", "", { "peerDependencies": { "@react-native-firebase/app": "22.4.0" } }, "sha512-iY+676RTwntRqq0CqcbGhidaegt/a6eKaoLTXeAxvtPYQaYXQL1fCDuZKfoy6uBfNsAGDxx2z4jYJuU+kOv4pA=="], @@ -624,6 +630,16 @@ "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], + "@tanstack/query-async-storage-persister": ["@tanstack/query-async-storage-persister@5.99.2", "", { "dependencies": { "@tanstack/query-core": "5.99.2", "@tanstack/query-persist-client-core": "5.99.2" } }, "sha512-FIr13Zv7GiMZGrdxoxOuzolT4xfyLrKWVBMfTZLMGJTc9IceFu2RT+EfH+j5jcKfvjB4T2no3qWSPGHxYmKKWg=="], + + "@tanstack/query-core": ["@tanstack/query-core@5.99.2", "", {}, "sha512-1HunU0bXVsR1ZJMZbcOPE6VtaBJxsW809RE9xPe4Gz7MlB0GWwQvuTPhMoEmQ/hIzFKJ/DWAuttIe7BOaWx0tA=="], + + "@tanstack/query-persist-client-core": ["@tanstack/query-persist-client-core@5.99.2", "", { "dependencies": { "@tanstack/query-core": "5.99.2" } }, "sha512-YYuLGBDGCsUbfN2LuYrfkRCpg1vOUZnK2bn4j7zAZv+m1B4CnLAv58Z3A43d5Cruxvld5udYFeYXw9F6g/pZcQ=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.99.2", "", { "dependencies": { "@tanstack/query-core": "5.99.2" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-vM91UEe45QUS9ED6OklsVL15i8qKcRqNwpWzPTVWvRPRSEgDudDgHpvyTjcdlwHcrKNa80T+xXYcchT2noPnZA=="], + + "@tanstack/react-query-persist-client": ["@tanstack/react-query-persist-client@5.99.2", "", { "dependencies": { "@tanstack/query-persist-client-core": "5.99.2" }, "peerDependencies": { "@tanstack/react-query": "^5.99.2", "react": "^18 || ^19" } }, "sha512-7+y5+kpaR26X2gdaEv0yQSFLZjqXz4Kn7wqzuYDQrb203b9MlYS3baML1M9hJTiLgi4QGGF2eJDdW8lHAazUow=="], + "@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="], "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], diff --git a/components/auth/viewLogin.tsx b/components/auth/viewLogin.tsx index ea358a2..02bd624 100644 --- a/components/auth/viewLogin.tsx +++ b/components/auth/viewLogin.tsx @@ -29,6 +29,7 @@ export default function ViewLogin({ onValidate }: Props) { setLoadingLogin(true) const response = await apiCheckPhoneLogin({ phone: `62${phone}` }) if (response.success) { + console.log({ response }) if (response.isWithoutOTP) { const encrypted = await encryptToken(response.id) signIn(encrypted) diff --git a/components/division/headerDivisionList.tsx b/components/division/headerDivisionList.tsx index d5d31b1..0b2f12d 100644 --- a/components/division/headerDivisionList.tsx +++ b/components/division/headerDivisionList.tsx @@ -33,7 +33,7 @@ export default function HeaderRightDivisionList() { }} /> { - (entityUser.role == "userRole" || entityUser.role == "developer") && + (entityUser.role == "supadmin" || entityUser.role == "developer") && } title="Filter" diff --git a/components/home/carouselHome2.tsx b/components/home/carouselHome2.tsx index 901cb45..a73ac74 100644 --- a/components/home/carouselHome2.tsx +++ b/components/home/carouselHome2.tsx @@ -5,7 +5,8 @@ import { setEntities } from "@/lib/bannerSlice"; import { setEntityUser } from "@/lib/userSlice"; import { useAuthSession } from "@/providers/AuthProvider"; import { useTheme } from "@/providers/ThemeProvider"; -import { AntDesign, Feather, FontAwesome5, Ionicons, MaterialCommunityIcons, MaterialIcons, } from "@expo/vector-icons"; +import { Feather, Ionicons } from "@expo/vector-icons"; +import { useQuery } from "@tanstack/react-query"; import { router } from "expo-router"; import React, { useEffect } from "react"; import { Dimensions, Image, View } from "react-native"; @@ -23,37 +24,44 @@ export default function CaraouselHome2({ refreshing }: { refreshing: boolean }) const progress = useSharedValue(0); const dispatch = useDispatch() const entities = useSelector((state: any) => state.banner) - const entityUser = useSelector((state: any) => state.user) - async function handleBannerView() { - const hasil = await decryptToken(String(token?.current)) - apiGetBanner({ user: hasil }).then((data) => { - if (data.data.length > 0) { - dispatch(setEntities(data.data)) - } else { - dispatch(setEntities([])) - } - }) - } + // Query for Banners + const { data: banners } = useQuery({ + queryKey: ['banners'], + queryFn: async () => { + const hasil = await decryptToken(String(token?.current)) + const data = await apiGetBanner({ user: hasil }) + return data.data || [] + }, + enabled: !!token?.current, + staleTime: 0, + }) - async function handleUser() { - const hasil = await decryptToken(String(token?.current)) - const response = await apiGetProfile({ id: hasil }) - dispatch(setEntityUser({ role: response.data.idUserRole, admin: false })) - } + // Query for Profile (Role Check) + const { data: profile } = useQuery({ + queryKey: ['profile'], // Shares same key as Home.tsx + queryFn: async () => { + const hasil = await decryptToken(String(token?.current)) + const data = await apiGetProfile({ id: hasil }) + return data.data + }, + enabled: !!token?.current, + staleTime: 0, + }) + // Sync Banners to Redux useEffect(() => { - if (refreshing) - handleBannerView() - }, [refreshing]); + if (banners) { + dispatch(setEntities(banners)) + } + }, [banners, dispatch]) + // Sync User Role to Redux useEffect(() => { - handleBannerView() - }, [dispatch]); - - useEffect(() => { - handleUser() - }, []); + if (profile) { + dispatch(setEntityUser({ role: profile.idUserRole, admin: false })) + } + }, [profile, dispatch]) return ( ([]) - const [maxValue, setMaxValue] = useState(5) - const [chartKey, setChartKey] = useState(0) - const barData = [ - { value: 23, label: 'Gambar', frontColor: '#fac858' }, - { value: 12, label: 'Dokumen', frontColor: '#92cc76' }, - ]; const width = Dimensions.get("window").width; - - async function handleData(loading: boolean) { - try { - setLoading(loading) + // TanStack Query for Document Chart data + const { data: chartData = [], isLoading } = useQuery({ + queryKey: ['homeData', 'dokumen'], + queryFn: async () => { const hasil = await decryptToken(String(token?.current)) const response = await apiGetDataHome({ cat: "dokumen", user: hasil }) - const maxVal = response.data.reduce((max: number, obj: { value: number; }) => Math.max(max, Number(obj.value)), 0); - const roundUp = maxVal > 0 ? Math.ceil(maxVal / 10) * 10 : 10; - setMaxValue(roundUp) - const convertedArray = response.data.map((item: { color: any; label: any; value: any; }) => ({ + return response.data.map((item: { color: any; label: any; value: any; }) => ({ frontColor: item.color, label: item.label, value: Number(item.value) - })); - setData(convertedArray) - setChartKey((prev: number) => prev + 1) - } catch (error) { - console.error(error) - } finally { - setLoading(false) - } - } + })) as Props + }, + enabled: !!token?.current, + staleTime: 0, + }) - useEffect(() => { - if (refreshing) { - handleData(false) - } - }, [refreshing]); - - useEffect(() => { - handleData(true) - }, []); + // Derived state for maxValue + const maxValue = useMemo(() => { + const maxVal = chartData.reduce((max: number, obj: { value: number; }) => Math.max(max, obj.value), 0); + return maxVal > 0 ? Math.ceil(maxVal / 10) * 10 : 10; + }, [chartData]); return ( JUMLAH DOKUMEN { - loading ? + isLoading ? : } - ) } \ No newline at end of file diff --git a/components/home/chartProgresHome.tsx b/components/home/chartProgresHome.tsx index f4a6c5f..78c58f3 100644 --- a/components/home/chartProgresHome.tsx +++ b/components/home/chartProgresHome.tsx @@ -2,7 +2,7 @@ import Styles from "@/constants/Styles"; import { apiGetDataHome } from "@/lib/api"; import { useAuthSession } from "@/providers/AuthProvider"; import { useTheme } from "@/providers/ThemeProvider"; -import { useEffect, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; import { View } from "react-native"; import { PieChart } from "react-native-gifted-charts"; import Skeleton from "../skeleton"; @@ -17,45 +17,32 @@ type Props = { export default function ChartProgresHome({ refreshing }: { refreshing: boolean }) { const { decryptToken, token } = useAuthSession() const { colors } = useTheme(); - const [data, setData] = useState([]) - const [loading, setLoading] = useState(true) - async function handleData(loading: boolean) { - try { - setLoading(loading) + // TanStack Query for Progress Chart data + const { data: chartData = [], isLoading } = useQuery({ + queryKey: ['homeData', 'progress'], + queryFn: async () => { const hasil = await decryptToken(String(token?.current)) const response = await apiGetDataHome({ cat: "progress", user: hasil }) - const convertedArray = response.data.map((item: { color: any; text: any; value: any; }) => ({ + return response.data.map((item: { color: any; text: any; value: any; }) => ({ color: item.color, text: item.text, value: Number(item.value) - })); - setData(convertedArray) - } catch (error) { - console.error(error) - } finally { - setLoading(false) - } - } - - useEffect(() => { - if (refreshing) - handleData(false) - }, [refreshing]); - - useEffect(() => { - handleData(true) - }, []); + })) as Props + }, + enabled: !!token?.current, + staleTime: 0, + }) return ( PROGRES KEGIATAN { - loading ? + isLoading ? : <> ([]) - const [loading, setLoading] = useState(true) const { colors } = useTheme(); - - async function handleData(loading: boolean) { - try { - setLoading(loading) + // TanStack Query for Discussion data + const { data: homeDiscussions = [], isLoading } = useQuery({ + queryKey: ['homeData', 'discussion'], + queryFn: async () => { const hasil = await decryptToken(String(token?.current)) const response = await apiGetDataHome({ cat: "discussion", user: hasil }) - setData(response.data) - } catch (error) { - console.error(error) - } finally { - setLoading(false) - } - } - - useEffect(() => { - if (refreshing) - handleData(false) - }, [refreshing]); - - useEffect(() => { - handleData(true) - }, []); + return response.data as Props[] + }, + enabled: !!token?.current, + staleTime: 0, + }) return ( Diskusi { - loading ? + isLoading ? <> : - data.length > 0 ? - data.map((item, index) => { + homeDiscussions.length > 0 ? + homeDiscussions.map((item: Props, index: number) => { return ( { router.push(`/division/${item.idDivision}/discussion/${item.id}`) }} /> ) diff --git a/components/home/divisionHome.tsx b/components/home/divisionHome.tsx index 137509a..4f342ef 100644 --- a/components/home/divisionHome.tsx +++ b/components/home/divisionHome.tsx @@ -3,8 +3,9 @@ import { apiGetDataHome } from "@/lib/api"; import { useAuthSession } from "@/providers/AuthProvider"; import { useTheme } from "@/providers/ThemeProvider"; import { Feather } from "@expo/vector-icons"; +import { useQuery } from "@tanstack/react-query"; import { router } from "expo-router"; -import React, { useEffect, useState } from "react"; +import React from "react"; import { Dimensions, Pressable, View } from "react-native"; import { ICarouselInstance } from "react-native-reanimated-carousel"; import Skeleton from "../skeleton"; @@ -21,45 +22,31 @@ export default function DivisionHome({ refreshing }: { refreshing: boolean }) { const { colors } = useTheme(); const ref = React.useRef(null) const width = Dimensions.get("window").width - const [data, setData] = useState([]) - const [loading, setLoading] = useState(true) const arrSkeleton = Array.from({ length: 2 }, (_, index) => index) - async function handleData(loading: boolean) { - try { - setLoading(loading) + // TanStack Query for Division data + const { data: homeDivisions = [], isLoading } = useQuery({ + queryKey: ['homeData', 'division'], + queryFn: async () => { const hasil = await decryptToken(String(token?.current)) const response = await apiGetDataHome({ cat: "division", user: hasil }) - setData(response.data) - } catch (error) { - console.error(error) - } finally { - setLoading(false) - } - } - - useEffect(() => { - if (refreshing) - handleData(false) - }, [refreshing]); - - useEffect(() => { - handleData(true) - }, []); - - + return response.data as Props[] + }, + enabled: !!token?.current, + staleTime: 0, + }) return ( Divisi Teraktif { - loading ? + isLoading ? arrSkeleton.map((item, index) => ( )) : - data.length > 0 ? - data.map((item, index) => ( + homeDivisions.length > 0 ? + homeDivisions.map((item, index) => ( { router.push(`/division/${item.id}`) }}> diff --git a/components/home/eventHome.tsx b/components/home/eventHome.tsx index 6e0927a..7848a0f 100644 --- a/components/home/eventHome.tsx +++ b/components/home/eventHome.tsx @@ -2,8 +2,8 @@ import Styles from "@/constants/Styles"; import { apiGetDataHome } from "@/lib/api"; import { useAuthSession } from "@/providers/AuthProvider"; import { useTheme } from "@/providers/ThemeProvider"; +import { useQuery } from "@tanstack/react-query"; import { router } from "expo-router"; -import { useEffect, useState } from "react"; import { View } from "react-native"; import EventItem from "../eventItem"; import Skeleton from "../skeleton"; @@ -26,30 +26,18 @@ type Props = { export default function EventHome({ refreshing }: { refreshing: boolean }) { const { decryptToken, token } = useAuthSession() const { colors } = useTheme(); - const [data, setData] = useState([]) - const [loading, setLoading] = useState(true) - async function handleData(loading: boolean) { - try { - setLoading(loading) + // TanStack Query for Event data + const { data: homeEvents = [], isLoading } = useQuery({ + queryKey: ['homeData', 'event'], + queryFn: async () => { const hasil = await decryptToken(String(token?.current)) const response = await apiGetDataHome({ cat: "event", user: hasil }) - setData(response.data) - } catch (error) { - console.error(error) - } finally { - setLoading(false) - } - } - - useEffect(() => { - if (refreshing) - handleData(false) - }, [refreshing]); - - useEffect(() => { - handleData(true) - }, []); + return response.data as Props[] + }, + enabled: !!token?.current, + staleTime: 0, + }) return ( @@ -57,14 +45,14 @@ export default function EventHome({ refreshing }: { refreshing: boolean }) { { - loading ? + isLoading ? <> : - data.length > 0 ? - data.map((item, index) => { + homeEvents.length > 0 ? + homeEvents.map((item: Props, index: number) => { return ( { router.push(`/division/${item.idDivision}/calendar/${item.id}`) }} title={item.title} user={item.user_name} jamAwal={item.timeStart} jamAkhir={item.timeEnd} /> ) diff --git a/components/home/projectHome.tsx b/components/home/projectHome.tsx index ad42093..debd327 100644 --- a/components/home/projectHome.tsx +++ b/components/home/projectHome.tsx @@ -2,8 +2,9 @@ import Styles from "@/constants/Styles"; import { apiGetDataHome } from "@/lib/api"; import { useAuthSession } from "@/providers/AuthProvider"; import { useTheme } from "@/providers/ThemeProvider"; +import { useQuery } from "@tanstack/react-query"; import { router } from "expo-router"; -import React, { useEffect, useState } from "react"; +import React from "react"; import { Dimensions, View } from "react-native"; import Carousel, { ICarouselInstance } from "react-native-reanimated-carousel"; import LabelStatus from "../labelStatus"; @@ -25,45 +26,33 @@ export default function ProjectHome({ refreshing }: { refreshing: boolean }) { const { decryptToken, token } = useAuthSession() const ref = React.useRef(null); const width = Dimensions.get("window").width; - const [data, setData] = useState([]) - const [loading, setLoading] = useState(true) const { colors } = useTheme(); - async function handleData(loading: boolean) { - try { - setLoading(loading) + // TanStack Query for Projects data + const { data: homeProjects = [], isLoading } = useQuery({ + queryKey: ['homeData', 'kegiatan'], + queryFn: async () => { const hasil = await decryptToken(String(token?.current)) const response = await apiGetDataHome({ cat: "kegiatan", user: hasil }) - setData(response.data) - } catch (error) { - console.error(error) - } finally { - setLoading(false) - } - } - - useEffect(() => { - if (refreshing) - handleData(false) - }, [refreshing]); - - useEffect(() => { - handleData(true) - }, []); + return response.data as Props[] + }, + enabled: !!token?.current, + staleTime: 0, + }) return ( Kegiatan Terupdate { - loading ? () + isLoading ? () : - data.length > 0 ? + homeProjects.length > 0 ? ( - { router.push(`/project/${data[index].id}`) }} title={data[index].title} headerColor="primary"> - + { router.push(`/project/${homeProjects[index].id}`) }} title={homeProjects[index].title} headerColor="primary"> + - {data[index].createdAt} + {homeProjects[index].createdAt} diff --git a/ios/Desa.xcodeproj/project.pbxproj b/ios/Desa.xcodeproj/project.pbxproj index b1cad5d..942742d 100644 --- a/ios/Desa.xcodeproj/project.pbxproj +++ b/ios/Desa.xcodeproj/project.pbxproj @@ -394,7 +394,7 @@ ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; PRODUCT_BUNDLE_IDENTIFIER = mobiledarmasaba.app; - PRODUCT_NAME = "Desa"; + PRODUCT_NAME = Desa; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Desa/Desa-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -429,7 +429,7 @@ ); OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; PRODUCT_BUNDLE_IDENTIFIER = mobiledarmasaba.app; - PRODUCT_NAME = "Desa"; + PRODUCT_NAME = Desa; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Desa/Desa-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 7b1b0a8..c1a059c 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3,7 +3,7 @@ PODS: - DoubleConversion (1.1.6) - EXApplication (6.1.5): - ExpoModulesCore - - EXConstants (17.1.7): + - EXConstants (17.1.8): - ExpoModulesCore - EXImageLoader (5.1.0): - ExpoModulesCore @@ -11,9 +11,9 @@ PODS: - EXJSONUtils (0.15.0) - EXManifests (0.16.6): - ExpoModulesCore - - EXNotifications (0.31.4): + - EXNotifications (0.31.5): - ExpoModulesCore - - Expo (53.0.20): + - Expo (53.0.27): - DoubleConversion - ExpoModulesCore - glog @@ -282,7 +282,7 @@ PODS: - ExpoModulesCore - ExpoHaptics (14.1.4): - ExpoModulesCore - - ExpoHead (5.1.4): + - ExpoHead (5.1.11): - ExpoModulesCore - ExpoImagePicker (16.1.4): - ExpoModulesCore @@ -324,7 +324,7 @@ PODS: - ExpoModulesCore - ExpoSymbols (0.4.5): - ExpoModulesCore - - ExpoSystemUI (5.0.10): + - ExpoSystemUI (5.0.11): - ExpoModulesCore - ExpoWebBrowser (14.2.0): - ExpoModulesCore @@ -1703,6 +1703,8 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - Yoga + - react-native-netinfo (12.0.1): + - React-Core - react-native-render-html (6.3.4): - React-Core - react-native-safe-area-context (5.4.0): @@ -2296,6 +2298,7 @@ DEPENDENCIES: - react-native-blob-util (from `../node_modules/react-native-blob-util`) - react-native-date-picker (from `../node_modules/react-native-date-picker`) - react-native-image-picker (from `../node_modules/react-native-image-picker`) + - "react-native-netinfo (from `../node_modules/@react-native-community/netinfo`)" - react-native-render-html (from `../node_modules/react-native-render-html`) - react-native-safe-area-context (from `../node_modules/react-native-safe-area-context`) - react-native-webview (from `../node_modules/react-native-webview`) @@ -2505,6 +2508,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-date-picker" react-native-image-picker: :path: "../node_modules/react-native-image-picker" + react-native-netinfo: + :path: "../node_modules/@react-native-community/netinfo" react-native-render-html: :path: "../node_modules/react-native-render-html" react-native-safe-area-context: @@ -2600,12 +2605,12 @@ SPEC CHECKSUMS: boost: 7e761d76ca2ce687f7cc98e698152abd03a18f90 DoubleConversion: cb417026b2400c8f53ae97020b2be961b59470cb EXApplication: 1e06972201838375ca1ec1ba34d586a98a5dc718 - EXConstants: 98bcf0f22b820f9b28f9fee55ff2daededadd2f8 + EXConstants: d3d551cb154718f5161c4247304e96aa59f6cca7 EXImageLoader: 4d3d3284141f1a45006cc4d0844061c182daf7ee EXJSONUtils: 1d3e4590438c3ee593684186007028a14b3686cd EXManifests: 691a779b04e4f2c96da46fb9bef4f86174fefcb5 - EXNotifications: be5e949edf1d60b70e77178b81aa505298fadd07 - Expo: 8685113c16058e8b3eb101dd52d6c8bca260bbea + EXNotifications: 6770976336aacdc7dc7aed7b538dd8f7ad2c55e8 + Expo: 052536aae777d5156739c960afd6aa54881df42a expo-dev-client: 9b1e78baf0dd87b005f035d180bbb07c05917fad expo-dev-launcher: 2f95084d36be3d9106790bea7a933a0d34210646 expo-dev-menu: 1456232a68c883078b61c02b7fa5b01d8a5ab840 @@ -2618,7 +2623,7 @@ SPEC CHECKSUMS: ExpoFileSystem: 7f92f7be2f5c5ed40a7c9efc8fa30821181d9d63 ExpoFont: cf508bc2e6b70871e05386d71cab927c8524cc8e ExpoHaptics: 0ff6e0d83cd891178a306e548da1450249d54500 - ExpoHead: a7b66cbaeeb51f4a85338d335a0f5467e29a2c90 + ExpoHead: cfc12096c9a68cbe25de93a8bfc4781c7689467e ExpoImagePicker: 0963da31800c906e01c03e25d7c849f16ebf02a2 ExpoKeepAwake: bf0811570c8da182bfb879169437d4de298376e7 ExpoLinearGradient: 7734c8059972fcf691fb4330bcdf3390960a152d @@ -2628,7 +2633,7 @@ SPEC CHECKSUMS: ExpoSharing: b0377be82430d07398c6a4cd60b5a15696accbd3 ExpoSplashScreen: 1c22c5d37647106e42d4ae1582bb6d0dda3b2385 ExpoSymbols: c5612a90fb9179cdaebcd19bea9d8c69e5d3b859 - ExpoSystemUI: c2724f9d5af6b1bb74e013efadf9c6a8fae547a2 + ExpoSystemUI: 433a971503b99020318518ed30a58204288bab2d ExpoWebBrowser: dc39a88485f007e61a3dff05d6a75f22ab4a2e92 EXUpdatesInterface: 7ff005b7af94ee63fa452ea7bb95d7a8ff40277a fast_float: 06eeec4fe712a76acc9376682e4808b05ce978b6 @@ -2683,6 +2688,7 @@ SPEC CHECKSUMS: react-native-blob-util: 45eb0e23b243b48955d231414ca5ee4da2439968 react-native-date-picker: 2eca217a8fb09c517f5bb6b23978718c6cec59ec react-native-image-picker: 0c4a539c4e67fbe3977916cd2c8d0e4c67f00a8c + react-native-netinfo: bed7e7b8f68e22e0862a77d7df28d31faa66375d react-native-render-html: 5afc4751f1a98621b3009432ef84c47019dcb2bd react-native-safe-area-context: 9d72abf6d8473da73033b597090a80b709c0b2f1 react-native-webview: 3df1192782174d1bd23f6a0f5a4fec3cdcca9954 diff --git a/package.json b/package.json index c108c70..5ddc145 100644 --- a/package.json +++ b/package.json @@ -24,12 +24,16 @@ "@react-native-clipboard/clipboard": "^1.16.3", "@react-native-community/cli": "^19.1.0", "@react-native-community/datetimepicker": "8.4.1", + "@react-native-community/netinfo": "^12.0.1", "@react-native-firebase/app": "^22.4.0", "@react-native-firebase/database": "^22.4.0", "@react-native-firebase/messaging": "^22.2.1", "@react-navigation/bottom-tabs": "^7.2.0", "@react-navigation/native": "^7.0.14", "@reduxjs/toolkit": "^2.7.0", + "@tanstack/query-async-storage-persister": "^5.99.2", + "@tanstack/react-query": "^5.99.2", + "@tanstack/react-query-persist-client": "^5.99.2", "@types/formidable": "^3.4.5", "axios": "^1.8.4", "crypto-es": "^2.1.0", diff --git a/providers/AuthProvider.tsx b/providers/AuthProvider.tsx index e9d4c02..0d49e97 100644 --- a/providers/AuthProvider.tsx +++ b/providers/AuthProvider.tsx @@ -52,12 +52,11 @@ export default function AuthProvider({ children }: { children: ReactNode }): Rea const signIn = useCallback(async (token: string) => { const hasil = await decryptToken(String(token)) - // const permission = await requestPermission() const permissionStorage = await AsyncStorage.getItem('@notification_permission') if (permissionStorage === "true") { const tokenDevice = await getToken() try { - const register = await apiRegisteredToken({ user: hasil, token: String(tokenDevice) }) + await apiRegisteredToken({ user: hasil, token: String(tokenDevice) }) } catch (error) { console.error(error) } finally { diff --git a/providers/QueryProvider.tsx b/providers/QueryProvider.tsx new file mode 100644 index 0000000..654a51c --- /dev/null +++ b/providers/QueryProvider.tsx @@ -0,0 +1,58 @@ +import React, { useEffect } from 'react'; +import { QueryClient, QueryClientProvider, focusManager, onlineManager } from '@tanstack/react-query'; +import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'; +import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'; +import AsyncStorage from '@react-native-async-storage/async-storage'; +import NetInfo from '@react-native-community/netinfo'; +import { AppState, Platform, AppStateStatus } from 'react-native'; + +// 1. Configure the QueryClient +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + // Data is considered stale after 5 minutes + staleTime: 5 * 60 * 1000, + // Keep unused data in cache for 24 hours + gcTime: 24 * 60 * 60 * 1000, + // Retry failed queries 2 times + retry: 2, + }, + }, +}); + +// 2. Configure the AsyncStorage persister +const asyncStoragePersister = createAsyncStoragePersister({ + storage: AsyncStorage, + // Key used to store cache in AsyncStorage + key: 'OFFLINE_CACHE', +}); + +// 3. Configure the Online Manager for NetInfo +onlineManager.setEventListener((setOnline) => { + return NetInfo.addEventListener((state) => { + setOnline(!!state.isConnected); + }); +}); + +// 4. Configure the Focus Manager for AppState +function onAppStateChange(status: AppStateStatus) { + if (Platform.OS !== 'web') { + focusManager.setFocused(status === 'active'); + } +} + +export default function QueryProvider({ children }: { children: React.ReactNode }) { + useEffect(() => { + const subscription = AppState.addEventListener('change', onAppStateChange); + return () => subscription.remove(); + }, []); + + return ( + + {children} + + ); +}