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>

View File

@@ -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=="],

View File

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

View File

@@ -33,7 +33,7 @@ export default function HeaderRightDivisionList() {
}}
/>
{
(entityUser.role == "userRole" || entityUser.role == "developer") &&
(entityUser.role == "supadmin" || entityUser.role == "developer") &&
<MenuItemRow
icon={<AntDesign name="filter" color={colors.text} size={25} />}
title="Filter"

View File

@@ -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<number>(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 (
<View

View File

@@ -2,7 +2,8 @@ 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 { useMemo } from "react";
import { Dimensions, View } from "react-native";
import { BarChart } from "react-native-gifted-charts";
import Skeleton from "../skeleton";
@@ -15,64 +16,44 @@ type Props = {
}[]
export default function ChartDokumenHome({ refreshing }: { refreshing: boolean }) {
const [loading, setLoading] = useState(true)
const { decryptToken, token } = useAuthSession()
const { colors } = useTheme();
const [data, setData] = useState<Props>([])
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 (
<View style={[Styles.wrapPaper, Styles.contentItemCenter, Styles.mb15, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
<Text style={[Styles.textSubtitle, Styles.mv15]}>JUMLAH DOKUMEN</Text>
{
loading ? <Skeleton width={100} height={200} borderRadius={10} widthType="percent" />
isLoading ? <Skeleton width={100} height={200} borderRadius={10} widthType="percent" />
:
<BarChart
key={chartKey}
showFractionalValues={false}
showYAxisIndices
noOfSections={4}
maxValue={maxValue}
data={data}
data={chartData}
isAnimated
width={width - 140}
barWidth={width * 0.25}
@@ -92,7 +73,6 @@ export default function ChartDokumenHome({ refreshing }: { refreshing: boolean }
}}
/>
}
</View>
)
}

View File

@@ -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<Props>([])
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 (
<View style={[Styles.wrapPaper, Styles.contentItemCenter, Styles.mb15, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
<Text style={[Styles.textSubtitle, Styles.mv15]}>PROGRES KEGIATAN</Text>
{
loading ? <Skeleton width={100} height={200} borderRadius={10} widthType="percent" />
isLoading ? <Skeleton width={100} height={200} borderRadius={10} widthType="percent" />
:
<>
<PieChart
data={data}
data={chartData}
showText
showValuesAsTooltipText
textColor={'black'}

View File

@@ -1,13 +1,13 @@
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 DiscussionItem from "../discussionItem";
import Skeleton from "../skeleton";
import Text from "../Text";
import { useTheme } from "@/providers/ThemeProvider";
type Props = {
id: string
@@ -20,46 +20,33 @@ type Props = {
export default function DisccussionHome({ refreshing }: { refreshing: boolean }) {
const { decryptToken, token } = useAuthSession()
const [data, setData] = useState<Props[]>([])
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 (
<View style={[Styles.mb15]}>
<Text style={[Styles.textDefaultSemiBold, Styles.mv10]}>Diskusi</Text>
<View style={[Styles.wrapPaper, { backgroundColor: colors.card, borderColor: colors.icon + '20' }, Styles.p0]}>
{
loading ?
isLoading ?
<>
<Skeleton width={100} height={70} borderRadius={10} widthType="percent" />
<Skeleton width={100} height={70} borderRadius={10} widthType="percent" />
</>
:
data.length > 0 ?
data.map((item, index) => {
homeDiscussions.length > 0 ?
homeDiscussions.map((item: Props, index: number) => {
return (
<DiscussionItem key={index} title={item.desc} user={item.user} date={item.date} onPress={() => { router.push(`/division/${item.idDivision}/discussion/${item.id}`) }} />
)

View File

@@ -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<ICarouselInstance>(null)
const width = Dimensions.get("window").width
const [data, setData] = useState<Props[]>([])
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 (
<View style={[Styles.mb15]}>
<Text style={[Styles.textDefaultSemiBold, Styles.mb10]}>Divisi Teraktif</Text>
{
loading ?
isLoading ?
arrSkeleton.map((item, index) => (
<Skeleton key={index} width={100} height={80} borderRadius={10} widthType="percent" />
))
:
data.length > 0 ?
data.map((item, index) => (
homeDivisions.length > 0 ?
homeDivisions.map((item, index) => (
<Pressable style={[Styles.wrapPaper, Styles.mb05, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]} key={index} onPress={() => { router.push(`/division/${item.id}`) }}>
<View style={[Styles.rowSpaceBetween, { alignItems: 'center' }]}>
<View>

View File

@@ -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<Props[]>([])
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 (
<View style={[Styles.mb15]}>
@@ -57,14 +45,14 @@ export default function EventHome({ refreshing }: { refreshing: boolean }) {
<View style={[Styles.wrapPaper, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
{
loading ?
isLoading ?
<>
<Skeleton width={100} height={70} borderRadius={10} widthType="percent" />
<Skeleton width={100} height={70} borderRadius={10} widthType="percent" />
</>
:
data.length > 0 ?
data.map((item, index) => {
homeEvents.length > 0 ?
homeEvents.map((item: Props, index: number) => {
return (
<EventItem key={index} category={index % 2 == 0 ? 'purple' : 'orange'} onPress={() => { router.push(`/division/${item.idDivision}/calendar/${item.id}`) }} title={item.title} user={item.user_name} jamAwal={item.timeStart} jamAkhir={item.timeEnd} />
)

View File

@@ -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<ICarouselInstance>(null);
const width = Dimensions.get("window").width;
const [data, setData] = useState<Props[]>([])
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 (
<View style={[Styles.mb05]}>
<Text style={[Styles.textDefaultSemiBold, Styles.mb10]}>Kegiatan Terupdate</Text>
{
loading ? (<Skeleton width={100} height={150} borderRadius={10} widthType="percent" />)
isLoading ? (<Skeleton width={100} height={150} borderRadius={10} widthType="percent" />)
:
data.length > 0 ?
homeProjects.length > 0 ?
<Carousel
ref={ref}
style={{ width: "100%" }}
width={width * 0.8}
height={220}
data={data}
data={homeProjects}
loop={false}
autoPlay={false}
autoPlayReverse={false}
@@ -71,24 +60,24 @@ export default function ProjectHome({ refreshing }: { refreshing: boolean }) {
snapEnabled={true}
vertical={false}
renderItem={({ index }) => (
<PaperGridContent titleTail={1} content="carousel" onPress={() => { router.push(`/project/${data[index].id}`) }} title={data[index].title} headerColor="primary">
<ProgressBar value={data[index].progress} category="carousel" />
<PaperGridContent titleTail={1} content="carousel" onPress={() => { router.push(`/project/${homeProjects[index].id}`) }} title={homeProjects[index].title} headerColor="primary">
<ProgressBar value={homeProjects[index].progress} category="carousel" />
<View style={[Styles.rowSpaceBetween]}>
<Text style={[Styles.textDefault, { color: colors.dimmed }]}>{data[index].createdAt}</Text>
<Text style={[Styles.textDefault, { color: colors.dimmed }]}>{homeProjects[index].createdAt}</Text>
<LabelStatus
size="default"
category={
data[index].status === 0 ? 'secondary' :
data[index].status === 1 ? 'warning' :
data[index].status === 2 ? 'success' :
data[index].status === 3 ? 'error' :
homeProjects[index].status === 0 ? 'secondary' :
homeProjects[index].status === 1 ? 'warning' :
homeProjects[index].status === 2 ? 'success' :
homeProjects[index].status === 3 ? 'error' :
'secondary'
}
text={
data[index].status === 0 ? 'SEGERA' :
data[index].status === 1 ? 'DIKERJAKAN' :
data[index].status === 2 ? 'SELESAI' :
data[index].status === 3 ? 'DIBATALKAN' :
homeProjects[index].status === 0 ? 'SEGERA' :
homeProjects[index].status === 1 ? 'DIKERJAKAN' :
homeProjects[index].status === 2 ? 'SELESAI' :
homeProjects[index].status === 3 ? 'DIBATALKAN' :
'SEGERA'
}
/>

View File

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

View File

@@ -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

View File

@@ -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",

View File

@@ -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 {

View File

@@ -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 (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister: asyncStoragePersister }}
>
{children}
</PersistQueryClientProvider>
);
}