Gunakan useRef untuk skip efek di render pertama sehingga refetch hanya dipanggil saat state update berubah (setelah CRUD), bukan setiap kali halaman dibuka.
156 lines
6.4 KiB
TypeScript
156 lines
6.4 KiB
TypeScript
import GuideOverlay from "@/components/GuideOverlay";
|
|
import InputSearch from "@/components/inputSearch";
|
|
import Skeleton from "@/components/skeleton";
|
|
import Text from '@/components/Text';
|
|
import Styles from "@/constants/Styles";
|
|
import { apiGetAnnouncement } from "@/lib/api";
|
|
import { GUIDE_ANNOUNCEMENT } from "@/lib/guideSteps";
|
|
import { useGuide } from "@/lib/useGuide";
|
|
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, useMemo, useRef, useState } from "react";
|
|
import { Pressable, RefreshControl, View, VirtualizedList } from "react-native";
|
|
import { useSelector } from "react-redux";
|
|
|
|
type Props = {
|
|
id: string
|
|
title: string
|
|
desc: string
|
|
createdAt: string
|
|
}
|
|
|
|
export default function Announcement() {
|
|
const { token, decryptToken } = useAuthSession()
|
|
const { colors } = useTheme();
|
|
const [search, setSearch] = useState('')
|
|
const update = useSelector((state: any) => state.announcementUpdate)
|
|
const isFirstRender = useRef(true)
|
|
const { visible: guideVisible, dismiss: dismissGuide } = useGuide('announcement')
|
|
const arrSkeleton = Array.from({ length: 5 }, (_, i) => i)
|
|
|
|
const themed = {
|
|
background: { backgroundColor: colors.background },
|
|
card: { backgroundColor: colors.card, borderColor: colors.icon + '18' },
|
|
iconBox: { backgroundColor: colors.icon + '18' },
|
|
title: { color: colors.text },
|
|
desc: { color: colors.dimmed },
|
|
date: { color: colors.dimmed },
|
|
cardPressed: { backgroundColor: colors.icon + '08' },
|
|
}
|
|
|
|
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, page: pageParam })
|
|
return response.data
|
|
},
|
|
initialPageParam: 1,
|
|
getNextPageParam: (lastPage, allPages) => {
|
|
return lastPage.length > 0 ? allPages.length + 1 : undefined
|
|
},
|
|
})
|
|
|
|
useEffect(() => {
|
|
if (isFirstRender.current) { isFirstRender.current = false; return }
|
|
refetch()
|
|
}, [update])
|
|
|
|
const flattenedData = useMemo(() => data?.pages.flat() || [], [data])
|
|
|
|
const getItem = (_data: unknown, index: number): Props => ({
|
|
id: flattenedData[index].id,
|
|
title: flattenedData[index].title,
|
|
desc: flattenedData[index].desc,
|
|
createdAt: flattenedData[index].createdAt,
|
|
})
|
|
|
|
const renderSkeleton = () => (
|
|
<View style={Styles.announcementListSkeletonCard}>
|
|
<View style={[Styles.rowSpaceBetween, Styles.rowItemsCenter, Styles.announcementListSkeletonHeader]}>
|
|
<View style={[Styles.rowItemsCenter, Styles.announcementListSkeletonTitleRow]}>
|
|
<Skeleton width={28} height={28} borderRadius={8} />
|
|
<Skeleton width={50} widthType="percent" height={12} borderRadius={6} />
|
|
</View>
|
|
<Skeleton width={15} widthType="percent" height={10} borderRadius={6} />
|
|
</View>
|
|
<Skeleton width={100} widthType="percent" height={10} borderRadius={6} />
|
|
<Skeleton width={80} widthType="percent" height={10} borderRadius={6} />
|
|
</View>
|
|
)
|
|
|
|
const renderItem = ({ item }: { item: Props }) => (
|
|
<Pressable
|
|
onPress={() => router.push(`/announcement/${item.id}`)}
|
|
style={({ pressed }) => [Styles.announcementListCard, themed.card, pressed && themed.cardPressed]}
|
|
>
|
|
<View style={[Styles.rowSpaceBetween, Styles.rowItemsCenter, Styles.announcementListCardHeader]}>
|
|
<View style={[Styles.rowItemsCenter, Styles.announcementListTitleRow]}>
|
|
<View style={[Styles.announcementListIconBox, themed.iconBox]}>
|
|
<MaterialIcons name="campaign" size={16} color={colors.icon} />
|
|
</View>
|
|
<Text style={[Styles.textDefaultSemiBold, Styles.announcementListTitleText, themed.title]} numberOfLines={1}>
|
|
{item.title}
|
|
</Text>
|
|
</View>
|
|
<Text style={[Styles.textInformation, Styles.announcementListDateText, themed.date]}>
|
|
{item.createdAt}
|
|
</Text>
|
|
</View>
|
|
<Text style={[Styles.textMediumNormal, Styles.announcementListDescText, themed.title]} numberOfLines={2}>
|
|
{item.desc.replace(/<[^>]*>?/gm, '').replace(/\r?\n|\r/g, ' ')}
|
|
</Text>
|
|
</Pressable>
|
|
)
|
|
|
|
return (
|
|
<View style={[Styles.flex1, Styles.announcementListContainer, themed.background]}>
|
|
<GuideOverlay visible={guideVisible} steps={GUIDE_ANNOUNCEMENT} onDismiss={dismissGuide} />
|
|
<InputSearch onChange={setSearch} />
|
|
<View style={[Styles.flex1, Styles.announcementListInner]}>
|
|
{isLoading && !flattenedData.length ? (
|
|
arrSkeleton.map((_, i) => (
|
|
<View key={i} style={[Styles.announcementListCard, themed.card]}>
|
|
{renderSkeleton()}
|
|
</View>
|
|
))
|
|
) : flattenedData.length > 0 ? (
|
|
<VirtualizedList
|
|
data={flattenedData}
|
|
getItemCount={() => flattenedData.length}
|
|
getItem={getItem}
|
|
renderItem={renderItem}
|
|
keyExtractor={(item, index) => String(item.id || index)}
|
|
onEndReached={() => { if (hasNextPage && !isFetchingNextPage) fetchNextPage() }}
|
|
onEndReachedThreshold={0.5}
|
|
showsVerticalScrollIndicator={false}
|
|
ItemSeparatorComponent={() => <View style={Styles.announcementListSeparator} />}
|
|
refreshControl={
|
|
<RefreshControl
|
|
refreshing={isRefetching && !isFetchingNextPage}
|
|
onRefresh={refetch}
|
|
tintColor={colors.icon}
|
|
/>
|
|
}
|
|
/>
|
|
) : (
|
|
<Text style={[Styles.textDefault, Styles.textCenter, Styles.mt30, themed.desc]}>
|
|
Tidak ada pengumuman
|
|
</Text>
|
|
)}
|
|
</View>
|
|
</View>
|
|
)
|
|
}
|