upd: caching data

Deskripsi:
- update caching pada fitur utama -yg fitur divisi belom
This commit is contained in:
2026-04-20 14:23:14 +08:00
parent 772551a917
commit ccf8ee1caf
27 changed files with 767 additions and 766 deletions

View File

@@ -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<Props[]>([])
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() {
</View>
<View style={[Styles.flex2, Styles.mt05]}>
{
loading ?
isLoading && !flattenedData.length ?
arrSkeleton.map((item, index) => {
return (
<SkeletonContent key={index} />
)
})
:
data.length > 0
flattenedData.length > 0
?
<VirtualizedList
data={data}
getItemCount={() => 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={
// <View style={[Styles.iconContent]}>
<MaterialIcons name="campaign" size={25} color={colors.text} />
// </View>
}
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={
<RefreshControl
refreshing={refreshing}
onRefresh={handleRefresh}
refreshing={isRefetching && !isFetchingNextPage}
onRefresh={refetch}
tintColor={colors.icon}
/>
}

View File

@@ -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
?
<View style={[Styles.p15, Styles.mb100]}>
{entities.map((index: any, key: number) => (
<BorderBottomItem
key={key}
onPress={() => {
setDataId(index.id)
setSelectFile(index)
setModal(true)
}}
borderType="all"
icon={
<Image
source={{ uri: `${ConstEnv.url_storage}/files/${index.image}` }}
style={[Styles.imgListBanner]}
/>
}
title={index.title}
/>
))}
</View>
:
<View style={[Styles.p15, Styles.mb100]}>
<Text style={[Styles.textDefault, Styles.textCenter]}>Tidak ada data</Text>
</View>
}
<View style={[Styles.p15, Styles.mb100]}>
{
isLoading ? (
<>
<Skeleton width={100} height={150} borderRadius={10} widthType="percent" />
<Skeleton width={100} height={150} borderRadius={10} widthType="percent" />
<Skeleton width={100} height={150} borderRadius={10} widthType="percent" />
</>
) :
entities.length > 0 ?
entities.map((index: any, key: number) => (
<BorderBottomItem
key={key}
onPress={() => {
setDataId(index.id)
setSelectFile(index)
setModal(true)
}}
borderType="all"
icon={
<Image
source={{ uri: `${ConstEnv.url_storage}/files/${index.image}` }}
style={[Styles.imgListBanner]}
/>
}
title={index.title}
/>
))
:
<View style={[Styles.p15, Styles.mb100]}>
<Text style={[Styles.textDefault, Styles.textCenter]}>Tidak ada data</Text>
</View>
}
</View>
</ScrollView>
<DrawerBottom animation="slide" isVisible={isModal} setVisible={() => setModal(false)} title="Menu">

View File

@@ -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<Props[]>([])
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() {
</View>
<View style={[Styles.flex2, Styles.mt05]}>
{
loading ?
isLoading ?
arrSkeleton.map((item: any, i: number) => {
return (
<SkeletonContent key={i} />
)
})
:
data.length > 0
flatData.length > 0
?
<VirtualizedList
data={data}
getItemCount={() => 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={
// <View style={[Styles.iconContent]}>
<MaterialIcons name="chat" size={25} color={colors.text} />
// </View>
}
title={item.title}
subtitle={
status != "false" && <LabelStatus category={item.status === 1 ? "success" : "error"} text={item.status === 1 ? "BUKA" : "TUTUP"} size="small" />
}
rightTopInfo={item.createdAt}
desc={item.desc.replace(/<[^>]*>?/gm, ' ').replace(/\r?\n|\r/g, ' ')}
desc={item.desc?.replace(/<[^>]*>?/gm, ' ').replace(/\r?\n|\r/g, ' ')}
leftBottomInfo={
<View style={[Styles.rowItemsCenter]}>
<Ionicons name="chatbox-ellipses-outline" size={18} color={colors.dimmed} style={Styles.mr05} />

View File

@@ -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<Props[]>([])
// ... 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() {
</View>
<View style={[{ flex: 2 }, Styles.mt10]}>
{
loading ?
isLoading ?
isList ?
arrSkeleton.map((item, index) => (
<SkeletonTwoItem key={index} />
@@ -216,7 +215,7 @@ export default function ListDivision() {
<Skeleton key={index} width={100} height={180} widthType="percent" borderRadius={10} />
))
:
data.length == 0 ? (
flatData.length == 0 ? (
<View style={[Styles.mt15]}>
<Text style={[Styles.textDefault, { textAlign: 'center', color: colors.dimmed }]}>Tidak ada data</Text>
</View>
@@ -224,9 +223,9 @@ export default function ListDivision() {
isList ? (
<View style={[Styles.h100]}>
<VirtualizedList
data={data}
data={flatData}
style={[{ paddingBottom: 100 }]}
getItemCount={() => data.length}
getItemCount={() => flatData.length}
getItem={getItem}
renderItem={({ item, index }: { item: Props, index: number }) => {
return (
@@ -260,9 +259,9 @@ export default function ListDivision() {
) : (
<View style={[Styles.h100]}>
<VirtualizedList
data={data}
data={flatData}
style={[{ paddingBottom: 100 }]}
getItemCount={() => data.length}
getItemCount={() => flatData.length}
getItem={getItem}
renderItem={({ item, index }: { item: Props, index: number }) => {
return (

View File

@@ -11,6 +11,8 @@ export default function Feature() {
const entityUser = useSelector((state: any) => state.user)
const { colors } = useTheme();
console.log({entityUser})
return (
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
<Stack.Screen

View File

@@ -15,7 +15,8 @@ import { setUpdateGroup } from "@/lib/groupSlice";
import { useAuthSession } from "@/providers/AuthProvider";
import { useTheme } from "@/providers/ThemeProvider";
import { AntDesign, Feather, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import { useEffect, useState } from "react";
import { useQuery, useQueryClient } from "@tanstack/react-query";
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,16 +32,14 @@ export default function Index() {
const { colors } = useTheme();
const [isModal, setModal] = useState(false)
const [isVisibleEdit, setVisibleEdit] = useState(false)
const [data, setData] = useState<Props[]>([])
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 (
<View style={[Styles.p15, { flex: 1, backgroundColor: colors.background }]}>
<View style={[Styles.mb10]}>
@@ -152,7 +157,7 @@ export default function Index() {
</View>
<View style={[{ flex: 2 }, Styles.mt10]}>
{
loading ?
isLoading ?
arrSkeleton.map((item, index) => {
return (
<SkeletonTwoItem key={index} />

View File

@@ -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)
};

View File

@@ -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<Props[]>([])
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() {
</View>
<View style={[{ flex: 2 }, Styles.mt10]}>
{
loading ?
isLoading ?
arrSkeleton.map((item, index) => {
return (
<SkeletonTwoItem key={index} />
)
})
:
data.length > 0
flatData.length > 0
?
<VirtualizedList
data={data}
getItemCount={() => data.length}
data={flatData}
getItemCount={() => flatData.length}
getItem={getItem}
renderItem={({ item, index }: { item: Props, index: number }) => {
return (

View File

@@ -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<Props[]>([])
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 (
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
<View style={[Styles.p15]}>
{
loading ?
isLoading ?
arrSkeleton.map((item, index) => {
return (
<SkeletonTwoItem key={index} />
)
})
:
data.length > 0 ?
flatData.length > 0 ?
<VirtualizedList
data={data}
getItemCount={() => data.length}
data={flatData}
getItemCount={() => flatData.length}
getItem={getItem}
renderItem={({ item, index }: { item: Props, index: number }) => {
return (

View File

@@ -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<Props[]>([])
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() {
</View>
<View style={[Styles.flex2, Styles.mt10]}>
{
loading ?
isLoading ?
arrSkeleton.map((item, index) => {
return (
<SkeletonTwoItem key={index} />

View File

@@ -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<Props[]>([])
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() {
</View>
<View style={[Styles.mt10]}>
{
// entityUser.role != 'cosupadmin' && entityUser.role != 'admin' &&
<View style={[Styles.rowOnly]}>
<Text style={[Styles.mr05]}>Filter :</Text>
{
@@ -218,18 +214,13 @@ export default function ListProject() {
: ''
}
<LabelStatus size="small" category="secondary" text={isYear} style={[Styles.mr05]} />
{/* {
(entityUser.role == 'user' || entityUser.role == 'coadmin')
? (cat == 'null' || cat == 'undefined' || cat == undefined || cat == '' || cat == 'data-saya') ? <LabelStatus size="small" category="primary" text="Kegiatan Saya" /> : <LabelStatus size="small" category="primary" text="Semua Kegiatan" />
: ''
} */}
</View>
}
</View>
</View>
<View style={[Styles.flex2, Styles.mt10]}>
{
loading ?
isLoading ?
isList ?
arrSkeleton.map((item, index) => (
<SkeletonTwoItem key={index} />
@@ -239,13 +230,13 @@ export default function ListProject() {
<Skeleton key={index} width={100} height={180} widthType="percent" borderRadius={10} />
))
:
data.length > 0
flatData.length > 0
?
isList ? (
<View style={[Styles.h100]}>
<VirtualizedList
data={data}
getItemCount={() => 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 (
<BorderBottomItem
key={index}
onPress={() => { router.push(`/project/${item.id}`); }}
borderType="bottom"
icon={
<View
style={[Styles.iconContent, ColorsStatus.lightGreen]}
>
<AntDesign
name="areachart"
size={25}
color={"#384288"}
/>
</View>
}
title={item.title}
/>
);
})
} */}
</View>
) : (
<View style={[Styles.h100]}>
<VirtualizedList
data={data}
getItemCount={() => 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 (
<PaperGridContent
key={index}
onPress={() => {
router.push(`/project/${item.id}`);
}}
content="page"
title={item.title}
headerColor="primary"
>
<ProgressBar value={item.progress} category="list" />
<View style={[Styles.rowSpaceBetween]}>
<Text style={[Styles.textDefault, { color: colors.dimmed }]}>
{item.createdAt}
</Text>
<LabelStatus
size="default"
category={
item.status === 0 ? 'primary' :
item.status === 1 ? 'warning' :
item.status === 2 ? 'success' :
item.status === 3 ? 'error' :
'primary'
}
text={
item.status === 0 ? 'SEGERA' :
item.status === 1 ? 'DIKERJAKAN' :
item.status === 2 ? 'SELESAI' :
item.status === 3 ? 'DIBATALKAN' :
'SEGERA'
}
/>
</View>
</PaperGridContent>
);
})} */}
</View>
)
:

View File

@@ -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() {
<GestureHandlerRootView style={Styles.flex1}>
<NotifierWrapper>
<ThemeProvider>
<AuthProvider>
<AppStack />
</AuthProvider>
<QueryProvider>
<AuthProvider>
<AppStack />
</AuthProvider>
</QueryProvider>
</ThemeProvider>
</NotifierWrapper>
</GestureHandlerRootView>