From 4eebf2f8936f4217faabf1802b3e19de89a64a40 Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Fri, 8 May 2026 14:18:53 +0800 Subject: [PATCH] feat: redesign halaman notifikasi dengan icon berwarna, grouping tanggal, dan urutan unread-first --- app/(application)/notification.tsx | 222 +++++++++++++++++++---------- 1 file changed, 147 insertions(+), 75 deletions(-) diff --git a/app/(application)/notification.tsx b/app/(application)/notification.tsx index c985a6f..c67f40b 100644 --- a/app/(application)/notification.tsx +++ b/app/(application)/notification.tsx @@ -1,7 +1,6 @@ -import BorderBottomItemVertical from "@/components/borderBottomItemVertical"; +import AppHeader from "@/components/AppHeader"; import SkeletonTwoItem from "@/components/skeletonTwoItem"; import Text from "@/components/Text"; -import { ColorsStatus } from "@/constants/ColorsStatus"; import Styles from "@/constants/Styles"; import { apiGetNotification, apiReadOneNotification } from "@/lib/api"; import { setUpdateNotification } from "@/lib/notificationSlice"; @@ -9,10 +8,12 @@ import { pushToPage } from "@/lib/pushToPage"; import { useAuthSession } from "@/providers/AuthProvider"; import { useTheme } from "@/providers/ThemeProvider"; import { Feather } from "@expo/vector-icons"; +import { router, Stack } from "expo-router"; import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; -import { useEffect, useMemo, useState } from "react"; -import { RefreshControl, SafeAreaView, View, VirtualizedList } from "react-native"; +import { useEffect, useMemo } from "react"; +import { FlatList, Pressable, RefreshControl, SafeAreaView, View } from "react-native"; import { useDispatch, useSelector } from "react-redux"; +import { useState } from "react"; type Props = { id: string @@ -24,6 +25,22 @@ type Props = { createdAt: string } +type HeaderRow = { _type: 'header'; date: string } +type ItemRow = Props & { _type: 'item' } +type ListRow = HeaderRow | ItemRow + +function getNotifStyle(category: string): { icon: keyof typeof Feather.glyphMap; color: string } { + if (category === 'announcement') return { icon: 'volume-2', color: '#3B82F6' } + if (category === 'project') return { icon: 'activity', color: '#10B981' } + if (category.includes('/task')) return { icon: 'check-circle', color: '#8B5CF6' } + if (category === 'division') return { icon: 'users', color: '#3B82F6' } + if (category.includes('/discussion') || category === 'discussion-general') return { icon: 'message-square', color: '#06B6D4' } + if (category.includes('/calendar')) return { icon: 'calendar', color: '#F59E0B' } + if (category.includes('/document')) return { icon: 'file-text', color: '#FBBF24' } + if (category === 'member') return { icon: 'user', color: '#1F3C88' } + return { icon: 'bell', color: '#6B7280' } +} + export default function Notification() { const { token, decryptToken } = useAuthSession() const { colors } = useTheme(); @@ -32,7 +49,6 @@ export default function Notification() { const updateNotification = useSelector((state: any) => state.notificationUpdate) const [refreshing, setRefreshing] = useState(false) - // TanStack Query for Notifications with Infinite Scroll const { data, fetchNextPage, @@ -55,12 +71,31 @@ export default function Notification() { staleTime: 0, }) - // Flatten pages into a single data array const flatData = useMemo(() => { return data?.pages.flatMap(page => page.data) || []; }, [data]) - // Refetch when manual update state changes + const listData = useMemo(() => { + const groups: Record = {} + const dateOrder: string[] = [] + + flatData.forEach((item) => { + if (!groups[item.createdAt]) { + groups[item.createdAt] = [] + dateOrder.push(item.createdAt) + } + groups[item.createdAt].push(item) + }) + + const result: ListRow[] = [] + dateOrder.forEach((date) => { + result.push({ _type: 'header', date }) + const sorted = [...groups[date]].sort((a, b) => Number(a.isRead) - Number(b.isRead)) + sorted.forEach((item) => result.push({ ...item, _type: 'item' })) + }) + return result + }, [flatData]) + useEffect(() => { refetch() }, [updateNotification, refetch]) @@ -71,16 +106,10 @@ export default function Notification() { setRefreshing(false) }; - 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 apiReadOneNotification({ user: hasil, id: id }) await queryClient.invalidateQueries({ queryKey: ['notifications'] }) pushToPage(category, idContent) dispatch(setUpdateNotification(!updateNotification)) @@ -89,70 +118,113 @@ export default function Notification() { } } - 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 ( - - { - isLoading ? - arrSkeleton.map((item, index) => { - return ( - - ) - }) - : - flatData.length > 0 ? - flatData.length} - getItem={getItem} - renderItem={({ item, index }: { item: Props, index: number }) => { - return ( - - - - } - title={item.title} - rightTopInfo={item.createdAt} - desc={item.desc} - textColor={item.isRead ? 'gray' : colors.text} - onPress={() => { - handleReadNotification(item.id, item.category, item.idContent) - }} - bgColor={'transparent'} - /> - ) - }} - keyExtractor={(item, index) => String(index)} - onEndReached={loadMoreData} - onEndReachedThreshold={0.5} - showsVerticalScrollIndicator={false} - refreshControl={ - - } + ( + router.back()} /> + ) + }} + /> + + + {isLoading ? ( + [0, 1, 2, 3, 4].map((_, i) => ) + ) : flatData.length === 0 ? ( + + + + Tidak ada notifikasi + + + ) : ( + String(index)} + showsVerticalScrollIndicator={false} + onEndReached={() => { + if (hasNextPage && !isFetchingNextPage) fetchNextPage() + }} + onEndReachedThreshold={0.5} + refreshControl={ + - : - Tidak ada data - } + } + renderItem={({ item }) => { + if (item._type === 'header') { + return ( + + + {item.date} + + + + ) + } + + const { icon, color } = getNotifStyle(item.category) + + return ( + handleReadNotification(item.id, item.category, item.idContent)} + style={({ pressed }) => [{ + flexDirection: 'row', + alignItems: 'center', + borderRadius: 10, + borderWidth: 1, + borderColor: colors.icon + '20', + backgroundColor: pressed + ? colors.icon + '10' + : item.isRead + ? colors.icon + '10' + : colors.card, + paddingHorizontal: 12, + paddingVertical: 10, + marginBottom: 6, + }]} + > + {/* Colored icon */} + + + + + {/* Content */} + + + + + {item.title} + + + + + {item.desc} + + + + ) + }} + /> + )} ) -} \ No newline at end of file +}