From d96c9545591a620b18cb31dd4b7256e36ad47cfe Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Fri, 8 May 2026 10:34:28 +0800 Subject: [PATCH 1/5] fix: tombol simpan edit anggota selalu disabled karena idGroup kosong --- app/(application)/member/edit/[id].tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/(application)/member/edit/[id].tsx b/app/(application)/member/edit/[id].tsx index cb45140..f9bd5d1 100644 --- a/app/(application)/member/edit/[id].tsx +++ b/app/(application)/member/edit/[id].tsx @@ -171,11 +171,9 @@ export default function EditMember() { } function checkForm() { - if (Object.values(error).some((v) => v == true) || Object.values(data).some((v) => v == "")) { - setDisableBtn(true) - } else { - setDisableBtn(false) - } + const requiredFields: (keyof Props)[] = ["idPosition", "idUserRole", "nik", "name", "email", "phone", "gender"]; + const hasEmpty = requiredFields.some((key) => data[key] === ""); + setDisableBtn(Object.values(error).some((v) => v === true) || hasEmpty); } useEffect(() => { -- 2.49.1 From bc2c89e0301d076f8eff36789f60768b525d3bd0 Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Fri, 8 May 2026 11:37:21 +0800 Subject: [PATCH 2/5] feat: redesign halaman pencarian dengan filter tabs, section badge, dan card style --- app/(application)/search.tsx | 341 +++++++++++++++++++++++------------ 1 file changed, 225 insertions(+), 116 deletions(-) diff --git a/app/(application)/search.tsx b/app/(application)/search.tsx index 6541b76..3f418f2 100644 --- a/app/(application)/search.tsx +++ b/app/(application)/search.tsx @@ -9,13 +9,12 @@ import Styles from "@/constants/Styles"; import { apiGetSearch } from "@/lib/api"; import { useAuthSession } from "@/providers/AuthProvider"; import { useTheme } from "@/providers/ThemeProvider"; -import { AntDesign, MaterialIcons } from "@expo/vector-icons"; +import { AntDesign, Feather, MaterialIcons } from "@expo/vector-icons"; import { router, Stack } from "expo-router"; import React, { useState } from "react"; -import { RefreshControl, SafeAreaView, ScrollView, View } from "react-native"; +import { RefreshControl, SafeAreaView, ScrollView, TouchableOpacity, View } from "react-native"; import Toast from "react-native-toast-message"; -// ... types ... type PropsUser = { id: string name: string @@ -38,6 +37,27 @@ type PropDivisi = { group: string } +type FilterType = "all" | "member" | "division" | "project" + +function SectionHeader({ label, count, colors }: { label: string; count: number; colors: any }) { + return ( + + + {label} + + + {count} + + + ) +} + export default function Search() { const { token, decryptToken } = useAuthSession() const [dataUser, setDataUser] = useState([]) @@ -45,11 +65,16 @@ export default function Search() { const [dataProject, setDataProject] = useState([]) const [refreshing, setRefreshing] = useState(false) const [search, setSearch] = useState('') + const [activeFilter, setActiveFilter] = useState("all") const { colors } = useTheme(); + const totalResults = dataUser.length + dataDivisi.length + dataProject.length + const hasSearch = search.length >= 3 + async function handleSearch(cari: string) { try { setSearch(cari) + setActiveFilter("all") if (cari.length >= 3) { const user = await decryptToken(String(token?.current)) const hasil = await apiGetSearch({ text: cari, user: user }) @@ -58,7 +83,7 @@ export default function Search() { setDataDivisi(hasil.data.division) setDataProject(hasil.data.project) } else { - return Toast.show({ type: 'small', text1: hasil.message, }) + return Toast.show({ type: 'small', text1: hasil.message }) } } else { setDataUser([]) @@ -68,15 +93,10 @@ export default function Search() { } catch (error: any) { console.error(error); const message = error?.response?.data?.message || "Gagal melakukan pencarian" - - Toast.show({ - type: 'small', - text1: message - }) + Toast.show({ type: 'small', text1: message }) } } - const handleRefresh = async () => { setRefreshing(true) handleSearch(search) @@ -84,114 +104,203 @@ export default function Search() { setRefreshing(false) }; + const filters: { key: FilterType; label: string; count: number }[] = [ + { key: "all", label: "Semua", count: totalResults }, + { key: "member", label: "Anggota", count: dataUser.length }, + { key: "division", label: "Divisi", count: dataDivisi.length }, + { key: "project", label: "Kegiatan", count: dataProject.length }, + ] + + const showUser = activeFilter === "all" || activeFilter === "member" + const showDivision = activeFilter === "all" || activeFilter === "division" + const showProject = activeFilter === "all" || activeFilter === "project" + + const activeFilterEmpty = + (activeFilter === "member" && dataUser.length === 0) || + (activeFilter === "division" && dataDivisi.length === 0) || + (activeFilter === "project" && dataProject.length === 0) return ( - <> - - ( - router.back()} /> - ) - }} - /> - + + ( + router.back()} /> + ) + }} + /> + + + {/* Search bar */} + - { - dataProject.length + dataDivisi.length + dataUser.length > 0 - ? - - } - > - { - dataUser.length > 0 && - - ANGGOTA - { - dataUser.map((item, index) => ( - } - title={item.name} - subtitle={`${item.group}-${item.position}`} - onPress={() => { - router.push(`/member/${item.id}`) - }} - /> - )) - } - - } - - { - dataDivisi.length > 0 && - - DIVISI - { - dataDivisi.map((item, index) => ( - - - - } - title={item.name} - subtitle={item.group} - onPress={() => { - router.push(`/division/${item.id}`) - }} - /> - )) - } - - } - - - { - dataProject.length > 0 && - - KEGIATAN - { - dataProject.map((item, index) => ( - - - - } - title={item.title} - subtitle={item.group} - onPress={() => { - router.push(`/project/${item.id}`) - }} - /> - )) - } - - } - - : - - Tidak ada data - - } - - - + + {/* Filter tabs */} + {hasSearch && totalResults > 0 && ( + + + {filters.map((f) => { + const isActive = activeFilter === f.key + return ( + setActiveFilter(f.key)} + style={{ + flexDirection: 'row', + alignItems: 'center', + paddingVertical: 6, + paddingHorizontal: 14, + borderRadius: 20, + marginRight: 8, + borderWidth: 1, + borderColor: isActive ? colors.tabActive : colors.icon + '40', + backgroundColor: isActive ? colors.tabActive + '20' : 'transparent', + }} + > + + {f.label} + + {f.count > 0 && ( + + + {f.count} + + + )} + + ) + })} + + + )} + + {/* Content */} + + {!hasSearch ? ( + + + + Ketik minimal 3 karakter untuk mencari + + + ) : totalResults === 0 ? ( + + + + Tidak ada hasil untuk "{search}" + + + ) : ( + + } + > + {/* Anggota */} + {showUser && dataUser.length > 0 && ( + + + {dataUser.map((item, index) => ( + + } + title={item.name} + subtitle={`${item.group} ยท ${item.position}`} + onPress={() => router.push(`/member/${item.id}`)} + colorPress + /> + + ))} + + )} + + {/* Divisi */} + {showDivision && dataDivisi.length > 0 && ( + + + {dataDivisi.map((item, index) => ( + + + + + } + title={item.name} + subtitle={item.group} + onPress={() => router.push(`/division/${item.id}`)} + colorPress + /> + + ))} + + )} + + {/* Kegiatan */} + {showProject && dataProject.length > 0 && ( + + + {dataProject.map((item, index) => ( + + + + + } + title={item.title} + subtitle={item.group} + onPress={() => router.push(`/project/${item.id}`)} + colorPress + /> + + ))} + + )} + + {/* Empty state untuk filter aktif */} + {activeFilter !== "all" && activeFilterEmpty && ( + + + + Tidak ada hasil di kategori ini + + + )} + + )} + + + ) -} \ No newline at end of file +} -- 2.49.1 From 4eebf2f8936f4217faabf1802b3e19de89a64a40 Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Fri, 8 May 2026 14:18:53 +0800 Subject: [PATCH 3/5] 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 +} -- 2.49.1 From 4af54980a0086600bd050af4db9140d719ad8f38 Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Fri, 8 May 2026 14:56:44 +0800 Subject: [PATCH 4/5] feat: tambah fitur tandai semua notifikasi dibaca dengan modal konfirmasi --- app/(application)/notification.tsx | 53 ++++++++++++++++++++++++++++-- lib/api.ts | 5 +++ 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/app/(application)/notification.tsx b/app/(application)/notification.tsx index c67f40b..64322f9 100644 --- a/app/(application)/notification.tsx +++ b/app/(application)/notification.tsx @@ -1,8 +1,9 @@ import AppHeader from "@/components/AppHeader"; +import ModalConfirmation from "@/components/ModalConfirmation"; import SkeletonTwoItem from "@/components/skeletonTwoItem"; import Text from "@/components/Text"; import Styles from "@/constants/Styles"; -import { apiGetNotification, apiReadOneNotification } from "@/lib/api"; +import { apiGetNotification, apiReadAllNotification, apiReadOneNotification } from "@/lib/api"; import { setUpdateNotification } from "@/lib/notificationSlice"; import { pushToPage } from "@/lib/pushToPage"; import { useAuthSession } from "@/providers/AuthProvider"; @@ -32,7 +33,7 @@ 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.includes('/task')) return { icon: 'clipboard', 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' } @@ -48,6 +49,8 @@ export default function Notification() { const dispatch = useDispatch() const updateNotification = useSelector((state: any) => state.notificationUpdate) const [refreshing, setRefreshing] = useState(false) + const [markingAll, setMarkingAll] = useState(false) + const [showConfirm, setShowConfirm] = useState(false) const { data, @@ -106,6 +109,22 @@ export default function Notification() { setRefreshing(false) }; + const hasUnread = flatData.some((item) => !item.isRead) + + async function handleReadAll() { + try { + setMarkingAll(true) + const hasil = await decryptToken(String(token?.current)) + await apiReadAllNotification({ user: hasil }) + await queryClient.invalidateQueries({ queryKey: ['notifications'] }) + dispatch(setUpdateNotification(!updateNotification)) + } catch (error) { + console.error(error) + } finally { + setMarkingAll(false) + } + } + async function handleReadNotification(id: string, category: string, idContent: string) { try { const hasil = await decryptToken(String(token?.current)) @@ -123,11 +142,39 @@ export default function Notification() { ( - router.back()} /> + router.back()} + right={ + hasUnread ? ( + setShowConfirm(true)} + disabled={markingAll} + style={{ opacity: markingAll ? 0.5 : 1, padding: 4 }} + > + + + ) : undefined + } + /> ) }} /> + { + setShowConfirm(false) + handleReadAll() + }} + onCancel={() => setShowConfirm(false)} + /> + {isLoading ? ( [0, 1, 2, 3, 4].map((_, i) => ) diff --git a/lib/api.ts b/lib/api.ts index 17a3967..cf3e82a 100644 --- a/lib/api.ts +++ b/lib/api.ts @@ -859,6 +859,11 @@ export const apiReadOneNotification = async (data: { user: string, id: string }) return response.data; }; +export const apiReadAllNotification = async (data: { user: string }) => { + const response = await api.post(`/mobile/home/notification`, data) + return response.data; +}; + export const apiGetVersion = async () => { const response = await api.get(`mobile/version`); return response.data; -- 2.49.1 From d31a21cc9c522e21e022155f304123be783663da Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Fri, 8 May 2026 16:52:26 +0800 Subject: [PATCH 5/5] upd: redesign --- app/(application)/discussion/[id].tsx | 139 +++-- app/(application)/discussion/index.tsx | 213 +++---- .../discussion/[detail]/index.tsx | 519 ++++++------------ .../(fitur-division)/discussion/index.tsx | 195 +++---- constants/Styles.ts | 56 ++ 5 files changed, 510 insertions(+), 612 deletions(-) diff --git a/app/(application)/discussion/[id].tsx b/app/(application)/discussion/[id].tsx index e47e18a..bc364bf 100644 --- a/app/(application)/discussion/[id].tsx +++ b/app/(application)/discussion/[id].tsx @@ -1,11 +1,9 @@ import AppHeader from "@/components/AppHeader"; -import BorderBottomItem from "@/components/borderBottomItem"; import BorderBottomItem2 from "@/components/borderBottomItem2"; import HeaderRightDiscussionGeneralDetail from "@/components/discussion_general/headerDiscussionDetail"; import DrawerBottom from "@/components/drawerBottom"; import ImageUser from "@/components/imageNew"; import { InputForm } from "@/components/inputForm"; -import LabelStatus from "@/components/labelStatus"; import MenuItemRow from "@/components/menuItemRow"; import ModalConfirmation from "@/components/ModalConfirmation"; import Skeleton from "@/components/skeleton"; @@ -271,29 +269,36 @@ export default function DetailDiscussionGeneral() { borderType="all" bgColor="white" icon={ - - + + } title={data?.title} titleShowAll={true} subtitle={ - !data?.isActive ? - - : - + + + {!data?.isActive ? 'Arsip' : data?.status == 1 ? 'Buka' : 'Tutup'} + + } desc={data?.desc} leftBottomInfo={ - - {dataKomentar.length} Komentar + + {dataKomentar.length} Komentar } rightBottomInfo={ - - {data?.createdAt} - + {data?.createdAt} } /> } @@ -306,36 +311,56 @@ export default function DetailDiscussionGeneral() { ) }) : - dataKomentar.map((item, i) => { - return ( - + dataKomentar.map((item, i) => ( + { + setDetailMore((prev: any) => + prev.includes(item.id) + ? prev.filter((id: string) => id !== item.id) + : [...prev, item.id] + ) + }} + onLongPress={() => { + item.idUser == entities.id && data?.status != 2 && data?.isActive && handleMenuKomentar(item.id, item.comment) + }} + style={({ pressed }) => [ + Styles.discussionCommentCard, + { + backgroundColor: pressed ? colors.icon + '10' : colors.card, + borderColor: colors.icon + '20', } - title={item.username} - rightTopInfo={item.createdAt} - desc={item.comment} - rightBottomInfo={item.isEdited ? "Edited" : ""} - descEllipsize={detailMore.includes(item.id) ? false : true} - bgColor="white" - onPress={() => { - setDetailMore((prev: any) => { - if (prev.includes(item.id)) { - return prev.filter((id: string) => id !== item.id) - } else { - return [...prev, item.id] - } - }) - }} - onLongPress={() => { - item.idUser == entities.id && data?.status != 2 && data?.isActive && handleMenuKomentar(item.id, item.comment) - }} - /> - ) - }) + ]} + > + + {/* Name + time */} + + + + + {item.username} + + {item.isEdited && ( + + diedit + + )} + + + {item.createdAt} + + + + {/* Comment text */} + + {item.comment} + + + + )) } @@ -372,15 +397,14 @@ export default function DetailDiscussionGeneral() { multiline focus={viewEdit} itemRight={ - { - (!loadingSendKomentar && selectKomentar.comment != '' && !regexOnlySpacesOrEnter.test(selectKomentar.comment) && data?.status === 1 && data?.isActive && (memberDiscussion || (entityUser.role != "user" && entityUser.role != "coadmin"))) - && handleEditKomentar() - }} - style={[ - Platform.OS == 'android' && Styles.mb12, - ]} + { + (!loadingSendKomentar && selectKomentar.comment != '' && !regexOnlySpacesOrEnter.test(selectKomentar.comment) && data?.status === 1 && data?.isActive && (memberDiscussion || (entityUser.role != "user" && entityUser.role != "coadmin"))) + && handleEditKomentar() + }} + style={[Platform.OS == 'android' && Styles.mb12]} > - + } /> @@ -398,15 +422,14 @@ export default function DetailDiscussionGeneral() { multiline focus={viewEdit} itemRight={ - { - (!loadingSendKomentar && komentar != '' && !regexOnlySpacesOrEnter.test(komentar) && data?.status === 1 && data?.isActive && (memberDiscussion || (entityUser.role != "user" && entityUser.role != "coadmin"))) - && handleKomentar() - }} - style={[ - Platform.OS == 'android' && Styles.mb12, - ]} + { + (!loadingSendKomentar && komentar != '' && !regexOnlySpacesOrEnter.test(komentar) && data?.status === 1 && data?.isActive && (memberDiscussion || (entityUser.role != "user" && entityUser.role != "coadmin"))) + && handleKomentar() + }} + style={[Platform.OS == 'android' && Styles.mb12]} > - + } /> diff --git a/app/(application)/discussion/index.tsx b/app/(application)/discussion/index.tsx index 14b521d..c7f4bb1 100644 --- a/app/(application)/discussion/index.tsx +++ b/app/(application)/discussion/index.tsx @@ -1,23 +1,20 @@ -import BorderBottomItem from "@/components/borderBottomItem"; import ButtonTab from "@/components/buttonTab"; import InputSearch from "@/components/inputSearch"; import LabelStatus from "@/components/labelStatus"; import SkeletonContent from "@/components/skeletonContent"; import Text from "@/components/Text"; import WrapTab from "@/components/wrapTab"; -import { ColorsStatus } from "@/constants/ColorsStatus"; import Styles from "@/constants/Styles"; 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 { AntDesign, Feather } from "@expo/vector-icons"; import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query"; import { router, useLocalSearchParams } from "expo-router"; import { useEffect, useMemo, useState } from "react"; -import { RefreshControl, View, VirtualizedList } from "react-native"; +import { FlatList, Pressable, RefreshControl, View } from "react-native"; import { useSelector } from "react-redux"; - type Props = { id: string title: string @@ -38,7 +35,6 @@ export default function Discussion() { const [status, setStatus] = useState<'true' | 'false'>(active == 'false' ? 'false' : 'true') const [refreshing, setRefreshing] = useState(false) - // TanStack Query for Discussions with Infinite Scroll const { data, fetchNextPage, @@ -50,12 +46,12 @@ export default function Discussion() { 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: pageParam + const response = await apiGetDiscussionGeneral({ + user: hasil, + active: status, + search: search, + group: String(group), + page: pageParam }) return response; }, @@ -67,17 +63,14 @@ export default function Discussion() { 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(() => { refetch() }, [update, refetch]) @@ -88,113 +81,129 @@ export default function Discussion() { setRefreshing(false) }; - const loadMoreData = () => { - if (hasNextPage && !isFetchingNextPage) { - fetchNextPage() - } - }; - - 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, - status: flatData[index]?.status, - total_komentar: flatData[index]?.total_komentar, - createdAt: flatData[index]?.createdAt, - }) + const isOpen = (item: Props) => item.status === 1 return ( - - - { - entityUser.role != "user" && entityUser.role != "coadmin" && + + {/* Header controls */} + + {entityUser.role != "user" && entityUser.role != "coadmin" && ( { setStatus("true") }} + onPress={() => setStatus("true")} label="Aktif" icon={} - n={2} /> + n={2} + /> { setStatus("false") }} + onPress={() => setStatus("false")} label="Arsip" icon={} - n={2} /> + n={2} + /> - } - + )} - { - (entityUser.role == "supadmin" || entityUser.role == "developer") && - - Filter : + {(entityUser.role == "supadmin" || entityUser.role == "developer") && ( + + Filter: - } + )} - - { - isLoading ? - arrSkeleton.map((item: any, i: number) => { - return ( - - ) - }) - : - flatData.length > 0 - ? - flatData.length} - getItem={getItem} - renderItem={({ item, index }: { item: Props, index: number }) => { - return ( - { router.push(`/discussion/${item.id}`) }} - borderType="bottom" - icon={ - - } - title={item.title} - subtitle={ - status != "false" && - } - rightTopInfo={item.createdAt} - desc={item.desc?.replace(/<[^>]*>?/gm, ' ').replace(/\r?\n|\r/g, ' ')} - leftBottomInfo={ - - - Diskusikan - - } - rightBottomInfo={`${item.total_komentar} Komentar`} - /> - ) - }} - keyExtractor={(item, index) => String(index)} - onEndReached={loadMoreData} - onEndReachedThreshold={0.5} - showsVerticalScrollIndicator={false} - refreshControl={ - - } + {/* List */} + + {isLoading ? ( + [0, 1, 2, 3, 4].map((_, i) => ) + ) : flatData.length === 0 ? ( + + + + Tidak ada diskusi + + + ) : ( + String(i)} + showsVerticalScrollIndicator={false} + onEndReached={() => { + if (hasNextPage && !isFetchingNextPage) fetchNextPage() + }} + onEndReachedThreshold={0.5} + refreshControl={ + - : - Tidak ada data - } + } + ItemSeparatorComponent={() => } + renderItem={({ item }: { item: Props }) => ( + router.push(`/discussion/${item.id}`)} + style={({ pressed }) => [ + Styles.discussionCard, + { + backgroundColor: pressed ? colors.icon + '10' : colors.card, + borderColor: colors.icon + '20', + } + ]} + > + {/* Top row: icon + title + status badge */} + + {/* Discussion icon */} + + + + + {/* Title + status badge */} + + + {item.title} + + {status !== "false" && ( + + + {isOpen(item) ? 'Buka' : 'Tutup'} + + + )} + + + + {/* Description */} + {item.desc ? ( + + {item.desc.replace(/<[^>]*>?/gm, ' ').replace(/\r?\n|\r/g, ' ')} + + ) : null} + + {/* Bottom row: comment count + date */} + + + + + {item.total_komentar} Komentar + + + + {item.createdAt} + + + + )} + /> + )} ); -} \ No newline at end of file +} diff --git a/app/(application)/division/[id]/(fitur-division)/discussion/[detail]/index.tsx b/app/(application)/division/[id]/(fitur-division)/discussion/[detail]/index.tsx index e85030f..bf1301b 100644 --- a/app/(application)/division/[id]/(fitur-division)/discussion/[detail]/index.tsx +++ b/app/(application)/division/[id]/(fitur-division)/discussion/[detail]/index.tsx @@ -1,11 +1,9 @@ import AppHeader from "@/components/AppHeader"; -import BorderBottomItem from "@/components/borderBottomItem"; import BorderBottomItem2 from "@/components/borderBottomItem2"; import HeaderRightDiscussionDetail from "@/components/discussion/headerDiscussionDetail"; import DrawerBottom from "@/components/drawerBottom"; import ImageUser from "@/components/imageNew"; import { InputForm } from "@/components/inputForm"; -import LabelStatus from "@/components/labelStatus"; import MenuItemRow from "@/components/menuItemRow"; import ModalConfirmation from "@/components/ModalConfirmation"; import Skeleton from "@/components/skeleton"; @@ -24,7 +22,7 @@ import { import { getDB } from "@/lib/firebaseDatabase"; import { useAuthSession } from "@/providers/AuthProvider"; import { useTheme } from "@/providers/ThemeProvider"; -import { Feather, Ionicons, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons"; +import { Feather, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons"; import { ref } from "@react-native-firebase/database"; import { useHeaderHeight } from '@react-navigation/elements'; import { router, Stack, useLocalSearchParams } from "expo-router"; @@ -87,24 +85,15 @@ export default function DiscussionDetail() { const [detailMore, setDetailMore] = useState([]) const entities = useSelector((state: any) => state.entities) const [isVisible, setVisible] = useState(false) - const [selectKomentar, setSelectKomentar] = useState({ - id: '', - comment: '' - }) + const [selectKomentar, setSelectKomentar] = useState({ id: '', comment: '' }) const [viewEdit, setViewEdit] = useState(false) const [showDeleteModal, setShowDeleteModal] = useState(false) - - useEffect(() => { const onValueChange = reference.on('value', snapshot => { - if (snapshot.val() == null) { - reference.set({ trigger: true }) - } + if (snapshot.val() == null) { reference.set({ trigger: true }) } handleLoadComment(false) }); - - // Stop listening for updates when no longer required return () => reference.off('value', onValueChange); }, []); @@ -115,23 +104,12 @@ export default function DiscussionDetail() { }); } - async function handleLoad(loading: boolean) { try { setLoading(loading) const hasil = await decryptToken(String(token?.current)); - const response = await apiGetDiscussionOne({ - id: detail, - user: hasil, - cat: "data", - }); - - const responseFile = await apiGetDiscussionOne({ - id: detail, - user: hasil, - cat: "file", - }); - + const response = await apiGetDiscussionOne({ id: detail, user: hasil, cat: "data" }); + const responseFile = await apiGetDiscussionOne({ id: detail, user: hasil, cat: "file" }); setData(response.data); setFileDiscussion(responseFile.data) setIsCreator(response.data.createdBy == hasil); @@ -146,11 +124,7 @@ export default function DiscussionDetail() { try { setLoadingKomentar(loading) const hasil = await decryptToken(String(token?.current)); - const response = await apiGetDiscussionOne({ - id: detail, - user: hasil, - cat: "comment", - }); + const response = await apiGetDiscussionOne({ id: detail, user: hasil, cat: "comment" }); setDataComment(response.data); } catch (error) { console.error(error); @@ -162,17 +136,8 @@ export default function DiscussionDetail() { async function handleCheckMember() { try { const hasil = await decryptToken(String(token?.current)); - const response = await apiGetDivisionOneFeature({ - id, - user: hasil, - cat: "check-member", - }); - - const response2 = await apiGetDivisionOneFeature({ - id, - user: hasil, - cat: "check-admin", - }); + const response = await apiGetDivisionOneFeature({ id, user: hasil, cat: "check-member" }); + const response2 = await apiGetDivisionOneFeature({ id, user: hasil, cat: "check-admin" }); setIsMemberDivision(response.data); setIsAdminDivision(response2.data); } catch (error) { @@ -180,33 +145,18 @@ export default function DiscussionDetail() { } } - useEffect(() => { - handleLoad(false); - }, [update.data]); - - useEffect(() => { - handleLoad(true) - handleLoadComment(true); - handleCheckMember(); - }, []); + useEffect(() => { handleLoad(false); }, [update.data]); + useEffect(() => { handleLoad(true); handleLoadComment(true); handleCheckMember(); }, []); async function handleKomentar() { try { setLoadingSend(true); const hasil = await decryptToken(String(token?.current)); - const response = await apiSendDiscussionCommentar({ - id: detail, - data: { comment: komentar, user: hasil }, - }); - if (response.success) { - setKomentar("") - updateTrigger() - } + const response = await apiSendDiscussionCommentar({ id: detail, data: { comment: komentar, user: hasil } }); + if (response.success) { setKomentar(""); updateTrigger() } } catch (error: any) { console.error(error); - const message = error?.response?.data?.message || "Gagal menambahkan komentar" - - Toast.show({ type: 'small', text1: message }) + Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal menambahkan komentar" }) } finally { setLoadingSend(false); } @@ -216,20 +166,11 @@ export default function DiscussionDetail() { try { setLoadingSend(true); const hasil = await decryptToken(String(token?.current)); - const response = await apiEditDiscussionCommentar({ - id: selectKomentar.id, - data: { comment: selectKomentar.comment, user: hasil }, - }); - if (response.success) { - updateTrigger() - } else { - Toast.show({ type: 'small', text1: response.message }) - } - } catch (error : any ) { + const response = await apiEditDiscussionCommentar({ id: selectKomentar.id, data: { comment: selectKomentar.comment, user: hasil } }); + if (response.success) { updateTrigger() } else { Toast.show({ type: 'small', text1: response.message }) } + } catch (error: any) { console.error(error); - const message = error?.response?.data?.message || "Gagal mengedit komentar" - - Toast.show({ type: 'small', text1: message }) + Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal mengedit komentar" }) } finally { setLoadingSend(false); handleViewEditKomentar() @@ -240,20 +181,11 @@ export default function DiscussionDetail() { try { setLoadingSend(true); const hasil = await decryptToken(String(token?.current)); - const response = await apiDeleteDiscussionCommentar({ - id: selectKomentar.id, - data: { user: hasil }, - }); - if (response.success) { - updateTrigger() - } else { - Toast.show({ type: 'small', text1: response.message }) - } - } catch (error : any ) { + const response = await apiDeleteDiscussionCommentar({ id: selectKomentar.id, data: { user: hasil } }); + if (response.success) { updateTrigger() } else { Toast.show({ type: 'small', text1: response.message }) } + } catch (error: any) { console.error(error); - const message = error?.response?.data?.message || "Gagal menghapus komentar" - - Toast.show({ type: 'small', text1: message }) + Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal menghapus komentar" }) } finally { setLoadingSend(false) setVisible(false) @@ -265,13 +197,11 @@ export default function DiscussionDetail() { setVisible(true) } - function handleViewEditKomentar() { setVisible(false) setViewEdit(!viewEdit) } - const handleRefresh = async () => { setRefreshing(true) handleLoad(false) @@ -280,27 +210,15 @@ export default function DiscussionDetail() { setRefreshing(false) }; + const canWrite = data?.status != 2 && data?.isActive && ((entityUser.role != "user" && entityUser.role != "coadmin") || isMemberDivision) + const isOpen = data?.status === 1 + return ( <> ( - // { - // router.back(); - // }} - // /> - // ), headerTitle: "Diskusi", headerTitleAlign: "center", - // headerRight: () => - // (entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision || isCreator ? - // : (<>) - // , header: () => ( router.back()} right={ ((entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision || isCreator) ? - : (<>) + : undefined } /> ) }} /> - + - } + showsVerticalScrollIndicator={false} + refreshControl={} > - - { - loading ? - - : - - } - title={data?.username} - subtitle={ - data?.isActive ? ( - data?.status == 1 ? ( - - ) : ( - + + {loading ? ( + + ) : ( + + } + title={data?.username} + titleShowAll={true} + subtitle={ + + + {!data?.isActive ? 'Arsip' : isOpen ? 'Buka' : 'Tutup'} + + + } + desc={data?.desc} + leftBottomInfo={ + + + {dataComment.length} Komentar + + } + rightBottomInfo={{data?.createdAt}} + /> + )} + + + {loadingKomentar ? ( + arrSkeleton.map((_, i) => ( + + )) + ) : ( + dataComment.map((item, i) => ( + { + setDetailMore((prev: any) => + prev.includes(item.id) ? prev.filter((id: string) => id !== item.id) : [...prev, item.id] ) - ) : ( - - ) - } - rightTopInfo={data?.createdAt} - desc={data?.desc} - leftBottomInfo={ - - - - {dataComment.length} Komentar + }} + onLongPress={() => { + item.idUser == entities.id && data?.status != 2 && data?.isActive && handleMenuKomentar(item.id, item.comment) + }} + style={({ pressed }) => [ + Styles.discussionCommentCard, + { backgroundColor: pressed ? colors.icon + '10' : colors.card, borderColor: colors.icon + '20' } + ]} + > + + + + + + {item.username} + + {item.isEdited && ( + diedit + )} + + + {item.createdAt} + + + + {item.comment} - } - /> - } - - - { - loadingKomentar ? - arrSkeleton.map((item, index) => ( - - )) - : - dataComment.map((item, index) => ( - - } - title={item.username} - rightTopInfo={item.createdAt} - desc={item.comment} - rightBottomInfo={item.isEdited ? "Edited" : ""} - descEllipsize={detailMore.includes(item.id) ? false : true} - bgColor="white" - onPress={() => { - setDetailMore((prev: any) => { - if (prev.includes(item.id)) { - return prev.filter((id: string) => id !== item.id) - } else { - return [...prev, item.id] - } - }) - }} - onLongPress={() => { - item.idUser == entities.id && data?.status != 2 && data?.isActive && handleMenuKomentar(item.id, item.comment) - }} - /> - )) - } - + + )) + )} - - - { - viewEdit ? - <> - - - - Edit Komentar - - handleViewEditKomentar()}> - - - - setSelectKomentar({ ...selectKomentar, comment: val })} - value={selectKomentar.comment} - itemRight={ - { - selectKomentar.comment != "" && - !regexOnlySpacesOrEnter.test(selectKomentar.comment) && - !loadingSend && - data?.status != 2 && - data?.isActive && - (((entityUser.role == "user" || - entityUser.role == "coadmin") && - isMemberDivision) || - entityUser.role == "admin" || - entityUser.role == "supadmin" || - entityUser.role == "developer" || - entityUser.role == "cosupadmin") && - handleEditKomentar(); - }} - style={[ - Platform.OS == 'android' && Styles.mb12, - ]} - > - - - } - /> - - : - data?.status != 2 && data?.isActive && ((entityUser.role != "user" && entityUser.role != "coadmin") || - isMemberDivision) - ? - { - komentar != "" && - !regexOnlySpacesOrEnter.test(komentar) && - !loadingSend && - data?.status != 2 && - data?.isActive && - (((entityUser.role == "user" || - entityUser.role == "coadmin") && - isMemberDivision) || - entityUser.role == "admin" || - entityUser.role == "supadmin" || - entityUser.role == "developer" || - entityUser.role == "cosupadmin") && - handleKomentar(); - }} - style={[ - Platform.OS == 'android' && Styles.mb12, - ]} - > - - - } - /> - : - - - { - data?.status == 2 ? "Diskusi telah ditutup" : data?.isActive == false ? "Diskusi telah diarsipkan" : "Hanya anggota divisi yang dapat memberikan komentar" - } - - - } + + + {viewEdit ? ( + <> + + + + Edit Komentar + + handleViewEditKomentar()}> + + + + setSelectKomentar({ ...selectKomentar, comment: val })} + value={selectKomentar.comment} + itemRight={ + { + selectKomentar.comment != "" && !regexOnlySpacesOrEnter.test(selectKomentar.comment) && !loadingSend && data?.status != 2 && data?.isActive + && (((entityUser.role == "user" || entityUser.role == "coadmin") && isMemberDivision) || entityUser.role == "admin" || entityUser.role == "supadmin" || entityUser.role == "developer" || entityUser.role == "cosupadmin") + && handleEditKomentar(); + }} + style={[Platform.OS == 'android' && Styles.mb12]} + > + + + } + /> + + ) : canWrite ? ( + { + komentar != "" && !regexOnlySpacesOrEnter.test(komentar) && !loadingSend && data?.status != 2 && data?.isActive + && (((entityUser.role == "user" || entityUser.role == "coadmin") && isMemberDivision) || entityUser.role == "admin" || entityUser.role == "supadmin" || entityUser.role == "developer" || entityUser.role == "cosupadmin") + && handleKomentar(); + }} + style={[Platform.OS == 'android' && Styles.mb12]} + > + + + } + /> + ) : ( + + + {data?.status == 2 ? "Diskusi telah ditutup" : data?.isActive == false ? "Diskusi telah diarsipkan" : "Hanya anggota divisi yang dapat memberikan komentar"} + + + )} + - } - title="Edit" - onPress={() => { handleViewEditKomentar() }} - /> - } - title="Hapus" - onPress={() => { - setVisible(false) - setTimeout(() => { - setShowDeleteModal(true) - }, 600) - }} - /> + } title="Edit" onPress={() => handleViewEditKomentar()} /> + } title="Hapus" onPress={() => { setVisible(false); setTimeout(() => setShowDeleteModal(true), 600) }} /> @@ -566,10 +404,7 @@ export default function DiscussionDetail() { visible={showDeleteModal} title="Konfirmasi" message="Apakah anda yakin ingin menghapus komentar?" - onConfirm={() => { - setShowDeleteModal(false) - handleDeleteKomentar() - }} + onConfirm={() => { setShowDeleteModal(false); handleDeleteKomentar() }} onCancel={() => setShowDeleteModal(false)} confirmText="Hapus" cancelText="Batal" diff --git a/app/(application)/division/[id]/(fitur-division)/discussion/index.tsx b/app/(application)/division/[id]/(fitur-division)/discussion/index.tsx index 43feec0..75dee0b 100644 --- a/app/(application)/division/[id]/(fitur-division)/discussion/index.tsx +++ b/app/(application)/division/[id]/(fitur-division)/discussion/index.tsx @@ -1,8 +1,6 @@ -import BorderBottomItem from "@/components/borderBottomItem"; import ButtonTab from "@/components/buttonTab"; import ImageUser from "@/components/imageNew"; import InputSearch from "@/components/inputSearch"; -import LabelStatus from "@/components/labelStatus"; import SkeletonContent from "@/components/skeletonContent"; import Text from "@/components/Text"; import WrapTab from "@/components/wrapTab"; @@ -11,13 +9,12 @@ import Styles from "@/constants/Styles"; import { apiGetDiscussion, apiGetDivisionOneFeature } from "@/lib/api"; import { useAuthSession } from "@/providers/AuthProvider"; import { useTheme } from "@/providers/ThemeProvider"; -import { AntDesign, Feather, Ionicons } from "@expo/vector-icons"; +import { AntDesign, Feather } from "@expo/vector-icons"; import { router, useLocalSearchParams } from "expo-router"; import { useEffect, useState } from "react"; -import { RefreshControl, View, VirtualizedList } from "react-native"; +import { FlatList, Pressable, RefreshControl, View } from "react-native"; import { useSelector } from "react-redux"; - type Props = { id: string, title: string, @@ -30,7 +27,6 @@ type Props = { isActive: boolean } - export default function DiscussionDivision() { const { colors } = useTheme(); const { id, active } = useLocalSearchParams<{ id: string, active?: string }>() @@ -51,17 +47,8 @@ export default function DiscussionDivision() { async function handleCheckMember() { try { const hasil = await decryptToken(String(token?.current)); - const response = await apiGetDivisionOneFeature({ - id, - user: hasil, - cat: "check-member", - }); - - const response2 = await apiGetDivisionOneFeature({ - id, - user: hasil, - cat: "check-admin", - }); + const response = await apiGetDivisionOneFeature({ id, user: hasil, cat: "check-member" }); + const response2 = await apiGetDivisionOneFeature({ id, user: hasil, cat: "check-admin" }); setIsMemberDivision(response.data); setIsAdminDivision(response2.data); } catch (error) { @@ -80,8 +67,6 @@ export default function DiscussionDivision() { setData(response.data) } else if (thisPage > 1 && response.data.length > 0) { setData([...data, ...response.data]) - } else { - return; } } catch (error) { console.error(error) @@ -91,26 +76,15 @@ export default function DiscussionDivision() { } } - useEffect(() => { - handleLoad(false, 1) - }, [update.data]) - - - useEffect(() => { - handleLoad(true, 1) - }, [status, search]) + useEffect(() => { handleLoad(false, 1) }, [update.data]) + useEffect(() => { handleLoad(true, 1) }, [status, search]) + useEffect(() => { handleCheckMember() }, []) const loadMoreData = () => { if (waiting) return - setTimeout(() => { - handleLoad(false, page + 1) - }, 1000); + setTimeout(() => { handleLoad(false, page + 1) }, 1000); } - useEffect(() => { - handleCheckMember() - }, []) - const handleRefresh = async () => { setRefreshing(true) handleLoad(false, 1) @@ -118,100 +92,101 @@ export default function DiscussionDivision() { setRefreshing(false) }; - const getItem = (_data: unknown, index: number): Props => ({ - id: data[index].id, - title: data[index].title, - desc: data[index].desc, - status: data[index].status, - user_name: data[index].user_name, - img: data[index].img, - total_komentar: data[index].total_komentar, - createdAt: data[index].createdAt, - isActive: data[index].isActive, - }) + const isOpen = (item: Props) => item.status === 1 return ( - - { - ((entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision) && - + + {((entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision) && ( + { setStatus("true") }} + onPress={() => setStatus("true")} label="Aktif" icon={} - n={2} /> + n={2} + /> { setStatus("false") }} + onPress={() => setStatus("false")} label="Arsip" icon={} - n={2} /> + n={2} + /> - } + )} + + {loading ? ( + arrSkeleton.map((_, i) => ) + ) : data.length === 0 ? ( + + + + Tidak ada diskusi + + + ) : ( + String(i)} + showsVerticalScrollIndicator={false} + onEndReached={loadMoreData} + onEndReachedThreshold={0.5} + refreshControl={ + + } + ItemSeparatorComponent={() => } + renderItem={({ item }: { item: Props }) => ( + router.push(`./discussion/${item.id}`)} + style={({ pressed }) => [ + Styles.discussionCard, + { backgroundColor: pressed ? colors.icon + '10' : colors.card, borderColor: colors.icon + '20' } + ]} + > + + + + + {item.user_name} + + {status === "true" && ( + + + {isOpen(item) ? 'Buka' : 'Tutup'} + + + )} + + - - { - loading ? - arrSkeleton.map((item: any, i: number) => { - return ( - - ) - }) - : - data.length > 0 ? - data.length} - getItem={getItem} - renderItem={({ item, index }: { item: Props, index: number }) => { - return ( - { router.push(`./discussion/${item.id}`) }} - borderType="bottom" - icon={ - - } - title={item.user_name} - subtitle={ - status == "true" ? item.status == 1 ? : : <> - } - rightTopInfo={item.createdAt} - desc={item.desc} - leftBottomInfo={ - - - Diskusikan - - } - rightBottomInfo={item.total_komentar + ' Komentar'} - bgColor="transparent" - /> - ) - }} - keyExtractor={(item, index) => String(index)} - onEndReached={loadMoreData} - onEndReachedThreshold={0.5} - showsVerticalScrollIndicator={false} - refreshControl={ - - } - /> - : - (Tidak ada diskusi) - } + {item.desc ? ( + + {item.desc} + + ) : null} + + + + + + {item.total_komentar} Komentar + + + + {item.createdAt} + + + + )} + /> + )} ); -} \ No newline at end of file +} diff --git a/constants/Styles.ts b/constants/Styles.ts index b9fab98..e3b2462 100644 --- a/constants/Styles.ts +++ b/constants/Styles.ts @@ -1113,6 +1113,62 @@ const Styles = StyleSheet.create({ flex: 1, marginLeft: 10, }, + discussionCard: { + borderRadius: 10, + borderWidth: 1, + padding: 14, + }, + discussionIconCircle: { + width: 40, + height: 40, + borderRadius: 20, + alignItems: 'center', + justifyContent: 'center', + flexShrink: 0, + }, + discussionIconCircleLg: { + width: 44, + height: 44, + borderRadius: 22, + alignItems: 'center', + justifyContent: 'center', + }, + discussionStatusPill: { + alignSelf: 'flex-start', + marginTop: 3, + paddingHorizontal: 8, + paddingVertical: 2, + borderRadius: 20, + borderWidth: 1, + }, + discussionStatusText: { + fontSize: 11, + fontWeight: '600', + }, + discussionCardIndent: { + marginLeft: 50, + }, + discussionSeparator: { + height: 8, + }, + discussionCommentText: { + fontSize: 12, + marginLeft: 5, + }, + discussionDateText: { + fontSize: 11, + }, + discussionCommentCard: { + borderRadius: 10, + borderWidth: 1, + padding: 12, + marginBottom: 8, + flexDirection: 'row', + }, + discussionEditedText: { + fontSize: 10, + fontStyle: 'italic', + }, }) export default Styles; \ No newline at end of file -- 2.49.1