- Ganti warna teks deskripsi dari dimmed ke text pada list diskusi umum dan divisi - Tambah class discussionHeaderPadding, discussionListPadding, discussionTitleCol, discussionDescMargin, discussionEmptyText ke component.styles.ts - Ganti semua inline style dengan themed object (warna dinamis) dan Styles.* (statis)
226 lines
9.6 KiB
TypeScript
226 lines
9.6 KiB
TypeScript
import GuideOverlay from "@/components/GuideOverlay";
|
|
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 Styles from "@/constants/Styles";
|
|
import { apiGetDiscussionGeneral } from "@/lib/api";
|
|
import { GUIDE_DISCUSSION } from "@/lib/guideSteps";
|
|
import { useGuide } from "@/lib/useGuide";
|
|
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, useMemo, useState } from "react";
|
|
import { FlatList, Pressable, RefreshControl, View } from "react-native";
|
|
import { useSelector } from "react-redux";
|
|
|
|
type Props = {
|
|
id: string
|
|
title: string
|
|
desc: string
|
|
status: number
|
|
total_komentar: number
|
|
createdAt: string
|
|
}
|
|
|
|
export default function Discussion() {
|
|
const entityUser = useSelector((state: any) => state.user)
|
|
const { token, decryptToken } = useAuthSession()
|
|
const { colors } = useTheme();
|
|
const { active, group } = useLocalSearchParams<{ active?: string, group?: string }>()
|
|
const [search, setSearch] = useState('')
|
|
const update = useSelector((state: any) => state.discussionGeneralDetailUpdate)
|
|
const queryClient = useQueryClient()
|
|
const [status, setStatus] = useState<'true' | 'false'>(active == 'false' ? 'false' : 'true')
|
|
const [refreshing, setRefreshing] = useState(false)
|
|
const { visible: guideVisible, dismiss: dismissGuide } = useGuide('discussion')
|
|
|
|
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: pageParam
|
|
})
|
|
return response;
|
|
},
|
|
initialPageParam: 1,
|
|
getNextPageParam: (lastPage, allPages) => {
|
|
return lastPage.data.length > 0 ? allPages.length + 1 : undefined;
|
|
},
|
|
enabled: !!token?.current,
|
|
staleTime: 0,
|
|
})
|
|
|
|
const flatData = useMemo(() => {
|
|
return data?.pages.flatMap(page => page.data) || [];
|
|
}, [data])
|
|
|
|
const nameGroup = useMemo(() => {
|
|
return data?.pages[0]?.filter?.name || "";
|
|
}, [data])
|
|
|
|
useEffect(() => {
|
|
refetch()
|
|
}, [update, refetch])
|
|
|
|
const handleRefresh = async () => {
|
|
setRefreshing(true)
|
|
await queryClient.invalidateQueries({ queryKey: ['discussions'] })
|
|
setRefreshing(false)
|
|
};
|
|
|
|
const isOpen = (item: Props) => item.status === 1
|
|
|
|
const themed = {
|
|
background: { backgroundColor: colors.background },
|
|
card: { backgroundColor: colors.card, borderColor: colors.icon + '20' },
|
|
cardPressed: { backgroundColor: colors.icon + '10' },
|
|
iconCircle: { backgroundColor: colors.icon + '20' },
|
|
title: { color: colors.text },
|
|
dimmed: { color: colors.dimmed },
|
|
statusOpen: { borderColor: '#10B981' as const },
|
|
statusClosed: { borderColor: colors.dimmed + '80' },
|
|
statusTextOpen: { color: '#10B981' as const },
|
|
statusTextClosed: { color: colors.dimmed },
|
|
}
|
|
|
|
return (
|
|
<View style={[Styles.flex1, themed.background]}>
|
|
<GuideOverlay visible={guideVisible} steps={GUIDE_DISCUSSION} onDismiss={dismissGuide} />
|
|
{/* Header controls */}
|
|
<View style={[Styles.ph15, Styles.discussionHeaderPadding]}>
|
|
{entityUser.role != "user" && entityUser.role != "coadmin" && (
|
|
<WrapTab>
|
|
<ButtonTab
|
|
active={status == "false" ? "false" : "true"}
|
|
value="true"
|
|
onPress={() => setStatus("true")}
|
|
label="Aktif"
|
|
icon={<Feather name="check-circle" color={status == "false" ? colors.dimmed : 'white'} size={20} />}
|
|
n={2}
|
|
/>
|
|
<ButtonTab
|
|
active={status == "false" ? "false" : "true"}
|
|
value="false"
|
|
onPress={() => setStatus("false")}
|
|
label="Arsip"
|
|
icon={<AntDesign name="closecircleo" color={status == "true" ? colors.dimmed : 'white'} size={20} />}
|
|
n={2}
|
|
/>
|
|
</WrapTab>
|
|
)}
|
|
<InputSearch onChange={setSearch} />
|
|
{(entityUser.role == "supadmin" || entityUser.role == "developer") && (
|
|
<View style={[Styles.mt10, Styles.rowOnly]}>
|
|
<Text>Filter :</Text>
|
|
<LabelStatus size="small" category="secondary" text={nameGroup} style={[Styles.mh05]} />
|
|
</View>
|
|
)}
|
|
</View>
|
|
|
|
{/* List */}
|
|
<View style={[Styles.flex1, Styles.ph15, Styles.discussionListPadding]}>
|
|
{isLoading ? (
|
|
[0, 1, 2, 3, 4].map((_, i) => <SkeletonContent key={i} />)
|
|
) : flatData.length === 0 ? (
|
|
<View style={[Styles.contentItemCenter, Styles.mt30]}>
|
|
<Feather name="message-circle" size={42} color={colors.icon + '40'} />
|
|
<Text style={[Styles.mt10, Styles.discussionEmptyText, themed.dimmed]}>
|
|
Tidak ada diskusi
|
|
</Text>
|
|
</View>
|
|
) : (
|
|
<FlatList
|
|
data={flatData}
|
|
keyExtractor={(_, i) => String(i)}
|
|
showsVerticalScrollIndicator={false}
|
|
onEndReached={() => {
|
|
if (hasNextPage && !isFetchingNextPage) fetchNextPage()
|
|
}}
|
|
onEndReachedThreshold={0.5}
|
|
refreshControl={
|
|
<RefreshControl
|
|
refreshing={refreshing}
|
|
onRefresh={handleRefresh}
|
|
tintColor={colors.icon}
|
|
/>
|
|
}
|
|
ItemSeparatorComponent={() => <View style={Styles.discussionSeparator} />}
|
|
renderItem={({ item }: { item: Props }) => (
|
|
<Pressable
|
|
onPress={() => router.push(`/discussion/${item.id}`)}
|
|
style={({ pressed }) => [
|
|
Styles.discussionCard,
|
|
themed.card,
|
|
pressed && themed.cardPressed,
|
|
]}
|
|
>
|
|
{/* Top row: icon + title + status badge */}
|
|
<View style={[Styles.rowItemsCenter, Styles.mb08]}>
|
|
{/* Discussion icon */}
|
|
<View style={[Styles.discussionIconCircle, themed.iconCircle]}>
|
|
<Feather name="message-circle" size={20} color={colors.icon} />
|
|
</View>
|
|
|
|
{/* Title + status badge */}
|
|
<View style={[Styles.flex1, Styles.discussionTitleCol]}>
|
|
<Text style={[Styles.textDefaultSemiBold, themed.title]} numberOfLines={1}>
|
|
{item.title}
|
|
</Text>
|
|
{status !== "false" && (
|
|
<View style={[Styles.discussionStatusPill, isOpen(item) ? themed.statusOpen : themed.statusClosed]}>
|
|
<Text style={[Styles.discussionStatusText, isOpen(item) ? themed.statusTextOpen : themed.statusTextClosed]}>
|
|
{isOpen(item) ? 'Buka' : 'Tutup'}
|
|
</Text>
|
|
</View>
|
|
)}
|
|
</View>
|
|
</View>
|
|
|
|
{/* Description */}
|
|
{item.desc ? (
|
|
<Text
|
|
style={[Styles.textMediumNormal, Styles.discussionCardIndent, Styles.discussionDescMargin, themed.title]}
|
|
numberOfLines={2}
|
|
>
|
|
{item.desc.replace(/<[^>]*>?/gm, ' ').replace(/\r?\n|\r/g, ' ')}
|
|
</Text>
|
|
) : null}
|
|
|
|
{/* Bottom row: comment count + date */}
|
|
<View style={[Styles.rowItemsCenter, Styles.rowSpaceBetween, Styles.discussionCardIndent]}>
|
|
<View style={Styles.rowItemsCenter}>
|
|
<Feather name="message-square" size={14} color={colors.dimmed} />
|
|
<Text style={[Styles.discussionCommentText, themed.dimmed]}>
|
|
{item.total_komentar} Komentar
|
|
</Text>
|
|
</View>
|
|
<Text style={[Styles.discussionDateText, themed.dimmed]}>
|
|
{item.createdAt}
|
|
</Text>
|
|
</View>
|
|
</Pressable>
|
|
)}
|
|
/>
|
|
)}
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|