amalia/08-mei-26 #46
@@ -1,11 +1,9 @@
|
|||||||
import AppHeader from "@/components/AppHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import BorderBottomItem from "@/components/borderBottomItem";
|
|
||||||
import BorderBottomItem2 from "@/components/borderBottomItem2";
|
import BorderBottomItem2 from "@/components/borderBottomItem2";
|
||||||
import HeaderRightDiscussionGeneralDetail from "@/components/discussion_general/headerDiscussionDetail";
|
import HeaderRightDiscussionGeneralDetail from "@/components/discussion_general/headerDiscussionDetail";
|
||||||
import DrawerBottom from "@/components/drawerBottom";
|
import DrawerBottom from "@/components/drawerBottom";
|
||||||
import ImageUser from "@/components/imageNew";
|
import ImageUser from "@/components/imageNew";
|
||||||
import { InputForm } from "@/components/inputForm";
|
import { InputForm } from "@/components/inputForm";
|
||||||
import LabelStatus from "@/components/labelStatus";
|
|
||||||
import MenuItemRow from "@/components/menuItemRow";
|
import MenuItemRow from "@/components/menuItemRow";
|
||||||
import ModalConfirmation from "@/components/ModalConfirmation";
|
import ModalConfirmation from "@/components/ModalConfirmation";
|
||||||
import Skeleton from "@/components/skeleton";
|
import Skeleton from "@/components/skeleton";
|
||||||
@@ -271,29 +269,36 @@ export default function DetailDiscussionGeneral() {
|
|||||||
borderType="all"
|
borderType="all"
|
||||||
bgColor="white"
|
bgColor="white"
|
||||||
icon={
|
icon={
|
||||||
<View style={[Styles.iconContent]}>
|
<View style={[Styles.discussionIconCircleLg, { backgroundColor: colors.icon + '20' }]}>
|
||||||
<MaterialIcons name="chat" size={25} color={'black'} />
|
<Feather name="message-circle" size={22} color={colors.icon} />
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
title={data?.title}
|
title={data?.title}
|
||||||
titleShowAll={true}
|
titleShowAll={true}
|
||||||
subtitle={
|
subtitle={
|
||||||
!data?.isActive ?
|
<View style={[Styles.discussionStatusPill, {
|
||||||
<LabelStatus category='warning' text='ARSIP' size="small" />
|
borderColor: !data?.isActive
|
||||||
:
|
? '#F59E0B'
|
||||||
<LabelStatus category={data.status == 1 ? 'success' : 'error'} text={data.status == 1 ? 'BUKA' : 'TUTUP'} size="small" />
|
: data?.status == 1 ? '#10B981' : colors.dimmed + '80',
|
||||||
|
}]}>
|
||||||
|
<Text style={[Styles.discussionStatusText, {
|
||||||
|
color: !data?.isActive
|
||||||
|
? '#F59E0B'
|
||||||
|
: data?.status == 1 ? '#10B981' : colors.dimmed,
|
||||||
|
}]}>
|
||||||
|
{!data?.isActive ? 'Arsip' : data?.status == 1 ? 'Buka' : 'Tutup'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
}
|
}
|
||||||
desc={data?.desc}
|
desc={data?.desc}
|
||||||
leftBottomInfo={
|
leftBottomInfo={
|
||||||
<View style={[Styles.rowItemsCenter]}>
|
<View style={[Styles.rowItemsCenter]}>
|
||||||
<Ionicons name="chatbox-ellipses-outline" size={18} color={colors.dimmed} style={Styles.mr05} />
|
<Feather name="message-square" size={14} color={colors.dimmed} style={Styles.mr05} />
|
||||||
<Text style={[Styles.textInformation, { color: colors.dimmed }, Styles.mb05]}>{dataKomentar.length} Komentar</Text>
|
<Text style={[Styles.textInformation, { color: colors.dimmed }]}>{dataKomentar.length} Komentar</Text>
|
||||||
</View>
|
</View>
|
||||||
}
|
}
|
||||||
rightBottomInfo={
|
rightBottomInfo={
|
||||||
<View style={[Styles.rowItemsCenter]}>
|
<Text style={[Styles.textInformation, { color: colors.dimmed }]}>{data?.createdAt}</Text>
|
||||||
<Text style={[Styles.textInformation, { color: colors.dimmed }, Styles.mb05]}>{data?.createdAt}</Text>
|
|
||||||
</View>
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -306,36 +311,56 @@ export default function DetailDiscussionGeneral() {
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
:
|
:
|
||||||
dataKomentar.map((item, i) => {
|
dataKomentar.map((item, i) => (
|
||||||
return (
|
<Pressable
|
||||||
<BorderBottomItem
|
key={i}
|
||||||
key={i}
|
onPress={() => {
|
||||||
borderType="all"
|
setDetailMore((prev: any) =>
|
||||||
colorPress
|
prev.includes(item.id)
|
||||||
icon={
|
? prev.filter((id: string) => id !== item.id)
|
||||||
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="xs" />
|
: [...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}
|
<View style={Styles.flex1}>
|
||||||
rightBottomInfo={item.isEdited ? "Edited" : ""}
|
{/* Name + time */}
|
||||||
descEllipsize={detailMore.includes(item.id) ? false : true}
|
<View style={[Styles.rowSpaceBetween, Styles.itemsCenter, Styles.mb05]}>
|
||||||
bgColor="white"
|
<View style={[Styles.rowItemsCenter, { gap: 8, flex: 1, marginRight: 8 }]}>
|
||||||
onPress={() => {
|
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="xs" />
|
||||||
setDetailMore((prev: any) => {
|
<Text style={[Styles.textMediumSemiBold, { color: colors.text }]} numberOfLines={1}>
|
||||||
if (prev.includes(item.id)) {
|
{item.username}
|
||||||
return prev.filter((id: string) => id !== item.id)
|
</Text>
|
||||||
} else {
|
{item.isEdited && (
|
||||||
return [...prev, item.id]
|
<Text style={[Styles.discussionEditedText, { color: colors.dimmed }]}>
|
||||||
}
|
diedit
|
||||||
})
|
</Text>
|
||||||
}}
|
)}
|
||||||
onLongPress={() => {
|
</View>
|
||||||
item.idUser == entities.id && data?.status != 2 && data?.isActive && handleMenuKomentar(item.id, item.comment)
|
<Text style={[Styles.discussionDateText, { color: colors.dimmed, flexShrink: 0 }]}>
|
||||||
}}
|
{item.createdAt}
|
||||||
/>
|
</Text>
|
||||||
)
|
</View>
|
||||||
})
|
|
||||||
|
{/* Comment text */}
|
||||||
|
<Text
|
||||||
|
style={[Styles.textDefault, { color: colors.text }]}
|
||||||
|
numberOfLines={detailMore.includes(item.id) ? 0 : 3}
|
||||||
|
>
|
||||||
|
{item.comment}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
))
|
||||||
}
|
}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@@ -372,15 +397,14 @@ export default function DetailDiscussionGeneral() {
|
|||||||
multiline
|
multiline
|
||||||
focus={viewEdit}
|
focus={viewEdit}
|
||||||
itemRight={
|
itemRight={
|
||||||
<Pressable onPress={() => {
|
<Pressable
|
||||||
(!loadingSendKomentar && selectKomentar.comment != '' && !regexOnlySpacesOrEnter.test(selectKomentar.comment) && data?.status === 1 && data?.isActive && (memberDiscussion || (entityUser.role != "user" && entityUser.role != "coadmin")))
|
onPress={() => {
|
||||||
&& handleEditKomentar()
|
(!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,
|
style={[Platform.OS == 'android' && Styles.mb12]}
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<MaterialIcons name="send" size={25} style={(loadingSendKomentar || selectKomentar.comment == '' || regexOnlySpacesOrEnter.test(selectKomentar.comment) || data?.status === 2 || !data?.isActive || (!memberDiscussion && (entityUser.role == "user" || entityUser.role == "coadmin"))) ? { color: colors.dimmed } : { color: colors.tint }} />
|
<MaterialIcons name="send" size={25} style={(loadingSendKomentar || selectKomentar.comment == '' || regexOnlySpacesOrEnter.test(selectKomentar.comment) || data?.status === 2 || !data?.isActive || (!memberDiscussion && (entityUser.role == "user" || entityUser.role == "coadmin"))) ? { color: colors.dimmed } : { color: colors.tint }} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -398,15 +422,14 @@ export default function DetailDiscussionGeneral() {
|
|||||||
multiline
|
multiline
|
||||||
focus={viewEdit}
|
focus={viewEdit}
|
||||||
itemRight={
|
itemRight={
|
||||||
<Pressable onPress={() => {
|
<Pressable
|
||||||
(!loadingSendKomentar && komentar != '' && !regexOnlySpacesOrEnter.test(komentar) && data?.status === 1 && data?.isActive && (memberDiscussion || (entityUser.role != "user" && entityUser.role != "coadmin")))
|
onPress={() => {
|
||||||
&& handleKomentar()
|
(!loadingSendKomentar && komentar != '' && !regexOnlySpacesOrEnter.test(komentar) && data?.status === 1 && data?.isActive && (memberDiscussion || (entityUser.role != "user" && entityUser.role != "coadmin")))
|
||||||
}}
|
&& handleKomentar()
|
||||||
style={[
|
}}
|
||||||
Platform.OS == 'android' && Styles.mb12,
|
style={[Platform.OS == 'android' && Styles.mb12]}
|
||||||
]}
|
|
||||||
>
|
>
|
||||||
<MaterialIcons name="send" size={25} style={(loadingSendKomentar || komentar == '' || regexOnlySpacesOrEnter.test(komentar) || data?.status === 2 || !data?.isActive || (!memberDiscussion && (entityUser.role == "user" || entityUser.role == "coadmin"))) ? { color: colors.dimmed } : { color: colors.tint }} />
|
<MaterialIcons name="send" size={25} style={(loadingSendKomentar || komentar == '' || regexOnlySpacesOrEnter.test(komentar) || data?.status === 2 || !data?.isActive || (!memberDiscussion && (entityUser.role == "user" || entityUser.role == "coadmin"))) ? { color: colors.dimmed } : { color: colors.tint }} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
import BorderBottomItem from "@/components/borderBottomItem";
|
|
||||||
import ButtonTab from "@/components/buttonTab";
|
import ButtonTab from "@/components/buttonTab";
|
||||||
import InputSearch from "@/components/inputSearch";
|
import InputSearch from "@/components/inputSearch";
|
||||||
import LabelStatus from "@/components/labelStatus";
|
import LabelStatus from "@/components/labelStatus";
|
||||||
import SkeletonContent from "@/components/skeletonContent";
|
import SkeletonContent from "@/components/skeletonContent";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
import WrapTab from "@/components/wrapTab";
|
import WrapTab from "@/components/wrapTab";
|
||||||
import { ColorsStatus } from "@/constants/ColorsStatus";
|
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiGetDiscussionGeneral } from "@/lib/api";
|
import { apiGetDiscussionGeneral } from "@/lib/api";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import { useTheme } from "@/providers/ThemeProvider";
|
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 { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { router, useLocalSearchParams } from "expo-router";
|
import { router, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
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";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
@@ -38,7 +35,6 @@ export default function Discussion() {
|
|||||||
const [status, setStatus] = useState<'true' | 'false'>(active == 'false' ? 'false' : 'true')
|
const [status, setStatus] = useState<'true' | 'false'>(active == 'false' ? 'false' : 'true')
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
|
||||||
// TanStack Query for Discussions with Infinite Scroll
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
@@ -67,17 +63,14 @@ export default function Discussion() {
|
|||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Flatten pages into a single data array
|
|
||||||
const flatData = useMemo(() => {
|
const flatData = useMemo(() => {
|
||||||
return data?.pages.flatMap(page => page.data) || [];
|
return data?.pages.flatMap(page => page.data) || [];
|
||||||
}, [data])
|
}, [data])
|
||||||
|
|
||||||
// Get nameGroup from the first available page
|
|
||||||
const nameGroup = useMemo(() => {
|
const nameGroup = useMemo(() => {
|
||||||
return data?.pages[0]?.filter?.name || "";
|
return data?.pages[0]?.filter?.name || "";
|
||||||
}, [data])
|
}, [data])
|
||||||
|
|
||||||
// Refetch when manual update state changes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refetch()
|
refetch()
|
||||||
}, [update, refetch])
|
}, [update, refetch])
|
||||||
@@ -88,112 +81,128 @@ export default function Discussion() {
|
|||||||
setRefreshing(false)
|
setRefreshing(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadMoreData = () => {
|
const isOpen = (item: Props) => item.status === 1
|
||||||
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,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[Styles.p15, Styles.flex1, { backgroundColor: colors.background }]}>
|
<View style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<View>
|
{/* Header controls */}
|
||||||
{
|
<View style={[Styles.ph15, { paddingTop: 12 }]}>
|
||||||
entityUser.role != "user" && entityUser.role != "coadmin" &&
|
{entityUser.role != "user" && entityUser.role != "coadmin" && (
|
||||||
<WrapTab>
|
<WrapTab>
|
||||||
<ButtonTab
|
<ButtonTab
|
||||||
active={status == "false" ? "false" : "true"}
|
active={status == "false" ? "false" : "true"}
|
||||||
value="true"
|
value="true"
|
||||||
onPress={() => { setStatus("true") }}
|
onPress={() => setStatus("true")}
|
||||||
label="Aktif"
|
label="Aktif"
|
||||||
icon={<Feather name="check-circle" color={status == "false" ? colors.dimmed : 'white'} size={20} />}
|
icon={<Feather name="check-circle" color={status == "false" ? colors.dimmed : 'white'} size={20} />}
|
||||||
n={2} />
|
n={2}
|
||||||
|
/>
|
||||||
<ButtonTab
|
<ButtonTab
|
||||||
active={status == "false" ? "false" : "true"}
|
active={status == "false" ? "false" : "true"}
|
||||||
value="false"
|
value="false"
|
||||||
onPress={() => { setStatus("false") }}
|
onPress={() => setStatus("false")}
|
||||||
label="Arsip"
|
label="Arsip"
|
||||||
icon={<AntDesign name="closecircleo" color={status == "true" ? colors.dimmed : 'white'} size={20} />}
|
icon={<AntDesign name="closecircleo" color={status == "true" ? colors.dimmed : 'white'} size={20} />}
|
||||||
n={2} />
|
n={2}
|
||||||
|
/>
|
||||||
</WrapTab>
|
</WrapTab>
|
||||||
}
|
)}
|
||||||
|
|
||||||
<InputSearch onChange={setSearch} />
|
<InputSearch onChange={setSearch} />
|
||||||
{
|
{(entityUser.role == "supadmin" || entityUser.role == "developer") && (
|
||||||
(entityUser.role == "supadmin" || entityUser.role == "developer") &&
|
<View style={[Styles.mv05, Styles.rowItemsCenter]}>
|
||||||
<View style={[Styles.mv05, Styles.rowOnly]}>
|
<Text style={{ color: colors.dimmed, fontSize: 12 }}>Filter:</Text>
|
||||||
<Text>Filter :</Text>
|
|
||||||
<LabelStatus size="small" category="secondary" text={nameGroup} style={[Styles.mh05]} />
|
<LabelStatus size="small" category="secondary" text={nameGroup} style={[Styles.mh05]} />
|
||||||
</View>
|
</View>
|
||||||
}
|
)}
|
||||||
</View>
|
</View>
|
||||||
<View style={[Styles.flex2, Styles.mt05]}>
|
|
||||||
{
|
|
||||||
isLoading ?
|
|
||||||
arrSkeleton.map((item: any, i: number) => {
|
|
||||||
return (
|
|
||||||
<SkeletonContent key={i} />
|
|
||||||
)
|
|
||||||
})
|
|
||||||
:
|
|
||||||
flatData.length > 0
|
|
||||||
?
|
|
||||||
<VirtualizedList
|
|
||||||
data={flatData}
|
|
||||||
getItemCount={() => flatData.length}
|
|
||||||
getItem={getItem}
|
|
||||||
renderItem={({ item, index }: { item: Props, index: number }) => {
|
|
||||||
return (
|
|
||||||
<BorderBottomItem
|
|
||||||
bgColor="transparent"
|
|
||||||
key={index}
|
|
||||||
onPress={() => { router.push(`/discussion/${item.id}`) }}
|
|
||||||
borderType="bottom"
|
|
||||||
icon={
|
|
||||||
<MaterialIcons name="chat" size={25} color={colors.text} />
|
|
||||||
}
|
|
||||||
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, ' ')}
|
|
||||||
leftBottomInfo={
|
|
||||||
<View style={[Styles.rowItemsCenter]}>
|
|
||||||
<Ionicons name="chatbox-ellipses-outline" size={18} color={colors.dimmed} style={Styles.mr05} />
|
|
||||||
<Text style={[Styles.textInformation, { color: colors.dimmed }, Styles.mb05]}>Diskusikan</Text>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
rightBottomInfo={`${item.total_komentar} Komentar`}
|
|
||||||
|
|
||||||
/>
|
{/* List */}
|
||||||
)
|
<View style={[Styles.flex1, Styles.ph15, { paddingTop: 8 }]}>
|
||||||
}}
|
{isLoading ? (
|
||||||
keyExtractor={(item, index) => String(index)}
|
[0, 1, 2, 3, 4].map((_, i) => <SkeletonContent key={i} />)
|
||||||
onEndReached={loadMoreData}
|
) : flatData.length === 0 ? (
|
||||||
onEndReachedThreshold={0.5}
|
<View style={[Styles.contentItemCenter, Styles.mt30]}>
|
||||||
showsVerticalScrollIndicator={false}
|
<Feather name="message-circle" size={42} color={colors.icon + '40'} />
|
||||||
refreshControl={
|
<Text style={[Styles.mt10, { color: colors.dimmed, fontSize: 14 }]}>
|
||||||
<RefreshControl
|
Tidak ada diskusi
|
||||||
refreshing={refreshing}
|
</Text>
|
||||||
onRefresh={handleRefresh}
|
</View>
|
||||||
tintColor={colors.icon}
|
) : (
|
||||||
/>
|
<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}
|
||||||
/>
|
/>
|
||||||
:
|
}
|
||||||
<Text style={[Styles.textDefault, Styles.textCenter, { color: colors.dimmed }]}>Tidak ada data</Text>
|
ItemSeparatorComponent={() => <View style={Styles.discussionSeparator} />}
|
||||||
}
|
renderItem={({ item }: { item: Props }) => (
|
||||||
|
<Pressable
|
||||||
|
onPress={() => 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 */}
|
||||||
|
<View style={[Styles.rowItemsCenter, Styles.mb08]}>
|
||||||
|
{/* Discussion icon */}
|
||||||
|
<View style={[Styles.discussionIconCircle, { backgroundColor: colors.icon + '20' }]}>
|
||||||
|
<Feather name="message-circle" size={20} color={colors.icon} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Title + status badge */}
|
||||||
|
<View style={[Styles.flex1, { marginLeft: 10 }]}>
|
||||||
|
<Text style={[Styles.textDefaultSemiBold, { color: colors.text }]} numberOfLines={1}>
|
||||||
|
{item.title}
|
||||||
|
</Text>
|
||||||
|
{status !== "false" && (
|
||||||
|
<View style={[Styles.discussionStatusPill, { borderColor: isOpen(item) ? '#10B981' : colors.dimmed + '80' }]}>
|
||||||
|
<Text style={[Styles.discussionStatusText, { color: isOpen(item) ? '#10B981' : colors.dimmed }]}>
|
||||||
|
{isOpen(item) ? 'Buka' : 'Tutup'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
{item.desc ? (
|
||||||
|
<Text
|
||||||
|
style={[Styles.textMediumNormal, Styles.discussionCardIndent, { color: colors.dimmed, marginBottom: 10 }]}
|
||||||
|
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, { color: colors.dimmed }]}>
|
||||||
|
{item.total_komentar} Komentar
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={[Styles.discussionDateText, { color: colors.dimmed }]}>
|
||||||
|
{item.createdAt}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import AppHeader from "@/components/AppHeader";
|
import AppHeader from "@/components/AppHeader";
|
||||||
import BorderBottomItem from "@/components/borderBottomItem";
|
|
||||||
import BorderBottomItem2 from "@/components/borderBottomItem2";
|
import BorderBottomItem2 from "@/components/borderBottomItem2";
|
||||||
import HeaderRightDiscussionDetail from "@/components/discussion/headerDiscussionDetail";
|
import HeaderRightDiscussionDetail from "@/components/discussion/headerDiscussionDetail";
|
||||||
import DrawerBottom from "@/components/drawerBottom";
|
import DrawerBottom from "@/components/drawerBottom";
|
||||||
import ImageUser from "@/components/imageNew";
|
import ImageUser from "@/components/imageNew";
|
||||||
import { InputForm } from "@/components/inputForm";
|
import { InputForm } from "@/components/inputForm";
|
||||||
import LabelStatus from "@/components/labelStatus";
|
|
||||||
import MenuItemRow from "@/components/menuItemRow";
|
import MenuItemRow from "@/components/menuItemRow";
|
||||||
import ModalConfirmation from "@/components/ModalConfirmation";
|
import ModalConfirmation from "@/components/ModalConfirmation";
|
||||||
import Skeleton from "@/components/skeleton";
|
import Skeleton from "@/components/skeleton";
|
||||||
@@ -24,7 +22,7 @@ import {
|
|||||||
import { getDB } from "@/lib/firebaseDatabase";
|
import { getDB } from "@/lib/firebaseDatabase";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import { useTheme } from "@/providers/ThemeProvider";
|
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 { ref } from "@react-native-firebase/database";
|
||||||
import { useHeaderHeight } from '@react-navigation/elements';
|
import { useHeaderHeight } from '@react-navigation/elements';
|
||||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||||
@@ -87,24 +85,15 @@ export default function DiscussionDetail() {
|
|||||||
const [detailMore, setDetailMore] = useState<any>([])
|
const [detailMore, setDetailMore] = useState<any>([])
|
||||||
const entities = useSelector((state: any) => state.entities)
|
const entities = useSelector((state: any) => state.entities)
|
||||||
const [isVisible, setVisible] = useState(false)
|
const [isVisible, setVisible] = useState(false)
|
||||||
const [selectKomentar, setSelectKomentar] = useState({
|
const [selectKomentar, setSelectKomentar] = useState({ id: '', comment: '' })
|
||||||
id: '',
|
|
||||||
comment: ''
|
|
||||||
})
|
|
||||||
const [viewEdit, setViewEdit] = useState(false)
|
const [viewEdit, setViewEdit] = useState(false)
|
||||||
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
const [showDeleteModal, setShowDeleteModal] = useState(false)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onValueChange = reference.on('value', snapshot => {
|
const onValueChange = reference.on('value', snapshot => {
|
||||||
if (snapshot.val() == null) {
|
if (snapshot.val() == null) { reference.set({ trigger: true }) }
|
||||||
reference.set({ trigger: true })
|
|
||||||
}
|
|
||||||
handleLoadComment(false)
|
handleLoadComment(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Stop listening for updates when no longer required
|
|
||||||
return () => reference.off('value', onValueChange);
|
return () => reference.off('value', onValueChange);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -115,23 +104,12 @@ export default function DiscussionDetail() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function handleLoad(loading: boolean) {
|
async function handleLoad(loading: boolean) {
|
||||||
try {
|
try {
|
||||||
setLoading(loading)
|
setLoading(loading)
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiGetDiscussionOne({
|
const response = await apiGetDiscussionOne({ id: detail, user: hasil, cat: "data" });
|
||||||
id: detail,
|
const responseFile = await apiGetDiscussionOne({ id: detail, user: hasil, cat: "file" });
|
||||||
user: hasil,
|
|
||||||
cat: "data",
|
|
||||||
});
|
|
||||||
|
|
||||||
const responseFile = await apiGetDiscussionOne({
|
|
||||||
id: detail,
|
|
||||||
user: hasil,
|
|
||||||
cat: "file",
|
|
||||||
});
|
|
||||||
|
|
||||||
setData(response.data);
|
setData(response.data);
|
||||||
setFileDiscussion(responseFile.data)
|
setFileDiscussion(responseFile.data)
|
||||||
setIsCreator(response.data.createdBy == hasil);
|
setIsCreator(response.data.createdBy == hasil);
|
||||||
@@ -146,11 +124,7 @@ export default function DiscussionDetail() {
|
|||||||
try {
|
try {
|
||||||
setLoadingKomentar(loading)
|
setLoadingKomentar(loading)
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiGetDiscussionOne({
|
const response = await apiGetDiscussionOne({ id: detail, user: hasil, cat: "comment" });
|
||||||
id: detail,
|
|
||||||
user: hasil,
|
|
||||||
cat: "comment",
|
|
||||||
});
|
|
||||||
setDataComment(response.data);
|
setDataComment(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -162,17 +136,8 @@ export default function DiscussionDetail() {
|
|||||||
async function handleCheckMember() {
|
async function handleCheckMember() {
|
||||||
try {
|
try {
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiGetDivisionOneFeature({
|
const response = await apiGetDivisionOneFeature({ id, user: hasil, cat: "check-member" });
|
||||||
id,
|
const response2 = await apiGetDivisionOneFeature({ id, user: hasil, cat: "check-admin" });
|
||||||
user: hasil,
|
|
||||||
cat: "check-member",
|
|
||||||
});
|
|
||||||
|
|
||||||
const response2 = await apiGetDivisionOneFeature({
|
|
||||||
id,
|
|
||||||
user: hasil,
|
|
||||||
cat: "check-admin",
|
|
||||||
});
|
|
||||||
setIsMemberDivision(response.data);
|
setIsMemberDivision(response.data);
|
||||||
setIsAdminDivision(response2.data);
|
setIsAdminDivision(response2.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -180,33 +145,18 @@ export default function DiscussionDetail() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => { handleLoad(false); }, [update.data]);
|
||||||
handleLoad(false);
|
useEffect(() => { handleLoad(true); handleLoadComment(true); handleCheckMember(); }, []);
|
||||||
}, [update.data]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleLoad(true)
|
|
||||||
handleLoadComment(true);
|
|
||||||
handleCheckMember();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
async function handleKomentar() {
|
async function handleKomentar() {
|
||||||
try {
|
try {
|
||||||
setLoadingSend(true);
|
setLoadingSend(true);
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiSendDiscussionCommentar({
|
const response = await apiSendDiscussionCommentar({ id: detail, data: { comment: komentar, user: hasil } });
|
||||||
id: detail,
|
if (response.success) { setKomentar(""); updateTrigger() }
|
||||||
data: { comment: komentar, user: hasil },
|
|
||||||
});
|
|
||||||
if (response.success) {
|
|
||||||
setKomentar("")
|
|
||||||
updateTrigger()
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
const message = error?.response?.data?.message || "Gagal menambahkan komentar"
|
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal menambahkan komentar" })
|
||||||
|
|
||||||
Toast.show({ type: 'small', text1: message })
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingSend(false);
|
setLoadingSend(false);
|
||||||
}
|
}
|
||||||
@@ -216,20 +166,11 @@ export default function DiscussionDetail() {
|
|||||||
try {
|
try {
|
||||||
setLoadingSend(true);
|
setLoadingSend(true);
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiEditDiscussionCommentar({
|
const response = await apiEditDiscussionCommentar({ id: selectKomentar.id, data: { comment: selectKomentar.comment, user: hasil } });
|
||||||
id: selectKomentar.id,
|
if (response.success) { updateTrigger() } else { Toast.show({ type: 'small', text1: response.message }) }
|
||||||
data: { comment: selectKomentar.comment, user: hasil },
|
} catch (error: any) {
|
||||||
});
|
|
||||||
if (response.success) {
|
|
||||||
updateTrigger()
|
|
||||||
} else {
|
|
||||||
Toast.show({ type: 'small', text1: response.message })
|
|
||||||
}
|
|
||||||
} catch (error : any ) {
|
|
||||||
console.error(error);
|
console.error(error);
|
||||||
const message = error?.response?.data?.message || "Gagal mengedit komentar"
|
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal mengedit komentar" })
|
||||||
|
|
||||||
Toast.show({ type: 'small', text1: message })
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingSend(false);
|
setLoadingSend(false);
|
||||||
handleViewEditKomentar()
|
handleViewEditKomentar()
|
||||||
@@ -240,20 +181,11 @@ export default function DiscussionDetail() {
|
|||||||
try {
|
try {
|
||||||
setLoadingSend(true);
|
setLoadingSend(true);
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiDeleteDiscussionCommentar({
|
const response = await apiDeleteDiscussionCommentar({ id: selectKomentar.id, data: { user: hasil } });
|
||||||
id: selectKomentar.id,
|
if (response.success) { updateTrigger() } else { Toast.show({ type: 'small', text1: response.message }) }
|
||||||
data: { user: hasil },
|
} catch (error: any) {
|
||||||
});
|
|
||||||
if (response.success) {
|
|
||||||
updateTrigger()
|
|
||||||
} else {
|
|
||||||
Toast.show({ type: 'small', text1: response.message })
|
|
||||||
}
|
|
||||||
} catch (error : any ) {
|
|
||||||
console.error(error);
|
console.error(error);
|
||||||
const message = error?.response?.data?.message || "Gagal menghapus komentar"
|
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal menghapus komentar" })
|
||||||
|
|
||||||
Toast.show({ type: 'small', text1: message })
|
|
||||||
} finally {
|
} finally {
|
||||||
setLoadingSend(false)
|
setLoadingSend(false)
|
||||||
setVisible(false)
|
setVisible(false)
|
||||||
@@ -265,13 +197,11 @@ export default function DiscussionDetail() {
|
|||||||
setVisible(true)
|
setVisible(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function handleViewEditKomentar() {
|
function handleViewEditKomentar() {
|
||||||
setVisible(false)
|
setVisible(false)
|
||||||
setViewEdit(!viewEdit)
|
setViewEdit(!viewEdit)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
setRefreshing(true)
|
setRefreshing(true)
|
||||||
handleLoad(false)
|
handleLoad(false)
|
||||||
@@ -280,27 +210,15 @@ export default function DiscussionDetail() {
|
|||||||
setRefreshing(false)
|
setRefreshing(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const canWrite = data?.status != 2 && data?.isActive && ((entityUser.role != "user" && entityUser.role != "coadmin") || isMemberDivision)
|
||||||
|
const isOpen = data?.status === 1
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
options={{
|
options={{
|
||||||
// headerLeft: () => (
|
|
||||||
// <ButtonBackHeader
|
|
||||||
// onPress={() => {
|
|
||||||
// router.back();
|
|
||||||
// }}
|
|
||||||
// />
|
|
||||||
// ),
|
|
||||||
headerTitle: "Diskusi",
|
headerTitle: "Diskusi",
|
||||||
headerTitleAlign: "center",
|
headerTitleAlign: "center",
|
||||||
// headerRight: () =>
|
|
||||||
// (entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision || isCreator ?
|
|
||||||
// <HeaderRightDiscussionDetail
|
|
||||||
// id={detail}
|
|
||||||
// status={data?.status}
|
|
||||||
// isActive={data?.isActive}
|
|
||||||
// /> : (<></>)
|
|
||||||
// ,
|
|
||||||
header: () => (
|
header: () => (
|
||||||
<AppHeader
|
<AppHeader
|
||||||
title="Diskusi"
|
title="Diskusi"
|
||||||
@@ -308,257 +226,177 @@ export default function DiscussionDetail() {
|
|||||||
onPressLeft={() => router.back()}
|
onPressLeft={() => router.back()}
|
||||||
right={
|
right={
|
||||||
((entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision || isCreator) ?
|
((entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision || isCreator) ?
|
||||||
<HeaderRightDiscussionDetail
|
<HeaderRightDiscussionDetail id={detail} status={data?.status} isActive={data?.isActive} /> : undefined
|
||||||
id={detail}
|
|
||||||
status={data?.status}
|
|
||||||
isActive={data?.isActive}
|
|
||||||
/> : (<></>)
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<View style={{ flex: 1, backgroundColor: colors.background }}>
|
<View style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
refreshControl={
|
showsVerticalScrollIndicator={false}
|
||||||
<RefreshControl
|
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} tintColor={colors.icon} />}
|
||||||
refreshing={refreshing}
|
|
||||||
onRefresh={handleRefresh}
|
|
||||||
tintColor={colors.icon}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<View style={[Styles.p15, Styles.mb100]}>
|
<View style={[Styles.p15]}>
|
||||||
{
|
{loading ? (
|
||||||
loading ?
|
<SkeletonContent />
|
||||||
<SkeletonContent />
|
) : (
|
||||||
:
|
<BorderBottomItem2
|
||||||
<BorderBottomItem2
|
dataFile={fileDiscussion}
|
||||||
dataFile={fileDiscussion}
|
descEllipsize={false}
|
||||||
descEllipsize={false}
|
borderType="all"
|
||||||
bgColor="white"
|
bgColor="white"
|
||||||
borderType="all"
|
icon={
|
||||||
icon={
|
<ImageUser src={`${ConstEnv.url_storage}/files/${data?.user_img}`} size="sm" />
|
||||||
<ImageUser
|
}
|
||||||
src={`${ConstEnv.url_storage}/files/${data?.user_img}`}
|
title={data?.username}
|
||||||
size="sm"
|
titleShowAll={true}
|
||||||
/>
|
subtitle={
|
||||||
}
|
<View style={[Styles.discussionStatusPill, {
|
||||||
title={data?.username}
|
borderColor: !data?.isActive ? '#F59E0B' : isOpen ? '#10B981' : colors.dimmed + '80',
|
||||||
subtitle={
|
}]}>
|
||||||
data?.isActive ? (
|
<Text style={[Styles.discussionStatusText, {
|
||||||
data?.status == 1 ? (
|
color: !data?.isActive ? '#F59E0B' : isOpen ? '#10B981' : colors.dimmed,
|
||||||
<LabelStatus category="success" text="BUKA" size="small" />
|
}]}>
|
||||||
) : (
|
{!data?.isActive ? 'Arsip' : isOpen ? 'Buka' : 'Tutup'}
|
||||||
<LabelStatus category="error" text="TUTUP" size="small" />
|
</Text>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
desc={data?.desc}
|
||||||
|
leftBottomInfo={
|
||||||
|
<View style={Styles.rowItemsCenter}>
|
||||||
|
<Feather name="message-square" size={14} color={colors.dimmed} style={Styles.mr05} />
|
||||||
|
<Text style={[Styles.textInformation, { color: colors.dimmed }]}>{dataComment.length} Komentar</Text>
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
rightBottomInfo={<Text style={[Styles.textInformation, { color: colors.dimmed }]}>{data?.createdAt}</Text>}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View style={Styles.mt10}>
|
||||||
|
{loadingKomentar ? (
|
||||||
|
arrSkeleton.map((_, i) => (
|
||||||
|
<Skeleton key={i} width={100} widthType="percent" height={40} borderRadius={5} />
|
||||||
|
))
|
||||||
|
) : (
|
||||||
|
dataComment.map((item, i) => (
|
||||||
|
<Pressable
|
||||||
|
key={i}
|
||||||
|
onPress={() => {
|
||||||
|
setDetailMore((prev: any) =>
|
||||||
|
prev.includes(item.id) ? prev.filter((id: string) => id !== item.id) : [...prev, item.id]
|
||||||
)
|
)
|
||||||
) : (
|
}}
|
||||||
<LabelStatus category="secondary" text="ARSIP" size="small" />
|
onLongPress={() => {
|
||||||
)
|
item.idUser == entities.id && data?.status != 2 && data?.isActive && handleMenuKomentar(item.id, item.comment)
|
||||||
}
|
}}
|
||||||
rightTopInfo={data?.createdAt}
|
style={({ pressed }) => [
|
||||||
desc={data?.desc}
|
Styles.discussionCommentCard,
|
||||||
leftBottomInfo={
|
{ backgroundColor: pressed ? colors.icon + '10' : colors.card, borderColor: colors.icon + '20' }
|
||||||
<View style={[Styles.rowItemsCenter]}>
|
]}
|
||||||
<Ionicons
|
>
|
||||||
name="chatbox-ellipses-outline"
|
<View style={Styles.flex1}>
|
||||||
size={18}
|
<View style={[Styles.rowSpaceBetween, Styles.itemsCenter, Styles.mb05]}>
|
||||||
color="grey"
|
<View style={[Styles.rowItemsCenter, { gap: 8, flex: 1, marginRight: 8 }]}>
|
||||||
style={Styles.mr05}
|
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="xs" />
|
||||||
/>
|
<Text style={[Styles.textMediumSemiBold, { color: colors.text }]} numberOfLines={1}>
|
||||||
<Text style={[Styles.textInformation, { color: colors.dimmed }, Styles.mb05]} >
|
{item.username}
|
||||||
{dataComment.length} Komentar
|
</Text>
|
||||||
|
{item.isEdited && (
|
||||||
|
<Text style={[Styles.discussionEditedText, { color: colors.dimmed }]}>diedit</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
<Text style={[Styles.discussionDateText, { color: colors.dimmed, flexShrink: 0 }]}>
|
||||||
|
{item.createdAt}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
<Text style={[Styles.textDefault, { color: colors.text }]} numberOfLines={detailMore.includes(item.id) ? 0 : 3}>
|
||||||
|
{item.comment}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
}
|
</Pressable>
|
||||||
/>
|
))
|
||||||
}
|
)}
|
||||||
|
|
||||||
<View style={[Styles.mt10]}>
|
|
||||||
{
|
|
||||||
loadingKomentar ?
|
|
||||||
arrSkeleton.map((item, index) => (
|
|
||||||
<Skeleton key={index} width={100} widthType="percent" height={40} borderRadius={5} />
|
|
||||||
))
|
|
||||||
:
|
|
||||||
dataComment.map((item, index) => (
|
|
||||||
<BorderBottomItem
|
|
||||||
key={index}
|
|
||||||
borderType="all"
|
|
||||||
colorPress
|
|
||||||
icon={
|
|
||||||
<ImageUser
|
|
||||||
src={`${ConstEnv.url_storage}/files/${item.img}`}
|
|
||||||
size="xs"
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<KeyboardAvoidingView
|
|
||||||
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
|
|
||||||
keyboardVerticalOffset={headerHeight}
|
|
||||||
>
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
Styles.contentItemCenter,
|
|
||||||
Styles.w100,
|
|
||||||
{ backgroundColor: colors.background },
|
|
||||||
viewEdit && Styles.borderTop
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
viewEdit ?
|
|
||||||
<>
|
|
||||||
<View style={[Styles.w90, Styles.rowSpaceBetween, Styles.pv05]}>
|
|
||||||
<View style={[Styles.rowItemsCenter]}>
|
|
||||||
<Feather name="edit-3" color={colors.text} size={22} style={[Styles.mh05]} />
|
|
||||||
<Text style={[Styles.textMediumSemiBold]}>Edit Komentar</Text>
|
|
||||||
</View>
|
|
||||||
<Pressable onPress={() => handleViewEditKomentar()}>
|
|
||||||
<MaterialIcons name="close" color={colors.text} size={22} />
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
<InputForm
|
|
||||||
bg={colors.card}
|
|
||||||
type="default"
|
|
||||||
round
|
|
||||||
multiline
|
|
||||||
placeholder="Kirim Komentar"
|
|
||||||
onChange={(val: string) => setSelectKomentar({ ...selectKomentar, comment: val })}
|
|
||||||
value={selectKomentar.comment}
|
|
||||||
itemRight={
|
|
||||||
<Pressable
|
|
||||||
onPress={() => {
|
|
||||||
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,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<MaterialIcons
|
|
||||||
name="send"
|
|
||||||
size={25}
|
|
||||||
style={
|
|
||||||
[selectKomentar.comment == "" || regexOnlySpacesOrEnter.test(selectKomentar.comment) || loadingSend || ((entityUser.role == "user" || entityUser.role == "coadmin") && !isMemberDivision)
|
|
||||||
? { color: colors.dimmed }
|
|
||||||
: { color: colors.tint },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Pressable>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
:
|
|
||||||
data?.status != 2 && data?.isActive && ((entityUser.role != "user" && entityUser.role != "coadmin") ||
|
|
||||||
isMemberDivision)
|
|
||||||
?
|
|
||||||
<InputForm
|
|
||||||
type="default"
|
|
||||||
round
|
|
||||||
multiline
|
|
||||||
placeholder="Kirim Komentar"
|
|
||||||
onChange={setKomentar}
|
|
||||||
value={komentar}
|
|
||||||
itemRight={
|
|
||||||
<Pressable
|
|
||||||
onPress={() => {
|
|
||||||
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,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<MaterialIcons
|
|
||||||
name="send"
|
|
||||||
size={25}
|
|
||||||
style={
|
|
||||||
[komentar == "" || regexOnlySpacesOrEnter.test(komentar) || loadingSend || ((entityUser.role == "user" || entityUser.role == "coadmin") && !isMemberDivision)
|
|
||||||
? { color: colors.dimmed }
|
|
||||||
: { color: colors.tint }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Pressable>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
:
|
|
||||||
<View style={[Styles.pv20, { alignItems: 'center' }]}>
|
|
||||||
<Text style={[Styles.textInformation, { color: colors.dimmed }]}>
|
|
||||||
{
|
|
||||||
data?.status == 2 ? "Diskusi telah ditutup" : data?.isActive == false ? "Diskusi telah diarsipkan" : "Hanya anggota divisi yang dapat memberikan komentar"
|
|
||||||
}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
<KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : undefined} keyboardVerticalOffset={headerHeight}>
|
||||||
|
<View style={[Styles.contentItemCenter, Styles.w100, { backgroundColor: colors.background }, viewEdit && Styles.borderTop]}>
|
||||||
|
{viewEdit ? (
|
||||||
|
<>
|
||||||
|
<View style={[Styles.w90, Styles.rowSpaceBetween, Styles.pv05]}>
|
||||||
|
<View style={Styles.rowItemsCenter}>
|
||||||
|
<Feather name="edit-3" color={colors.text} size={22} style={Styles.mh05} />
|
||||||
|
<Text style={Styles.textMediumSemiBold}>Edit Komentar</Text>
|
||||||
|
</View>
|
||||||
|
<Pressable onPress={() => handleViewEditKomentar()}>
|
||||||
|
<MaterialIcons name="close" color={colors.text} size={22} />
|
||||||
|
</Pressable>
|
||||||
|
</View>
|
||||||
|
<InputForm
|
||||||
|
bg={colors.card}
|
||||||
|
type="default" round multiline
|
||||||
|
placeholder="Kirim Komentar"
|
||||||
|
onChange={(val: string) => setSelectKomentar({ ...selectKomentar, comment: val })}
|
||||||
|
value={selectKomentar.comment}
|
||||||
|
itemRight={
|
||||||
|
<Pressable
|
||||||
|
onPress={() => {
|
||||||
|
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]}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="send" size={25}
|
||||||
|
style={[
|
||||||
|
selectKomentar.comment == "" || regexOnlySpacesOrEnter.test(selectKomentar.comment) || loadingSend || ((entityUser.role == "user" || entityUser.role == "coadmin") && !isMemberDivision)
|
||||||
|
? { color: colors.dimmed } : { color: colors.tint },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : canWrite ? (
|
||||||
|
<InputForm
|
||||||
|
type="default" round multiline
|
||||||
|
placeholder="Kirim Komentar"
|
||||||
|
onChange={setKomentar} value={komentar}
|
||||||
|
itemRight={
|
||||||
|
<Pressable
|
||||||
|
onPress={() => {
|
||||||
|
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]}
|
||||||
|
>
|
||||||
|
<MaterialIcons name="send" size={25}
|
||||||
|
style={[
|
||||||
|
komentar == "" || regexOnlySpacesOrEnter.test(komentar) || loadingSend || ((entityUser.role == "user" || entityUser.role == "coadmin") && !isMemberDivision)
|
||||||
|
? { color: colors.dimmed } : { color: colors.tint },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<View style={[Styles.pv20, Styles.itemsCenter]}>
|
||||||
|
<Text style={[Styles.textInformation, { color: colors.dimmed }]}>
|
||||||
|
{data?.status == 2 ? "Diskusi telah ditutup" : data?.isActive == false ? "Diskusi telah diarsipkan" : "Hanya anggota divisi yang dapat memberikan komentar"}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<DrawerBottom animation="slide" isVisible={isVisible} setVisible={setVisible} title="Komentar">
|
<DrawerBottom animation="slide" isVisible={isVisible} setVisible={setVisible} title="Komentar">
|
||||||
<View style={Styles.rowItemsCenter}>
|
<View style={Styles.rowItemsCenter}>
|
||||||
<MenuItemRow
|
<MenuItemRow icon={<MaterialCommunityIcons name="pencil-outline" color={colors.text} size={25} />} title="Edit" onPress={() => handleViewEditKomentar()} />
|
||||||
icon={<MaterialCommunityIcons name="pencil-outline" color={colors.text} size={25} />}
|
<MenuItemRow icon={<MaterialIcons name="delete-outline" color={colors.text} size={25} />} title="Hapus" onPress={() => { setVisible(false); setTimeout(() => setShowDeleteModal(true), 600) }} />
|
||||||
title="Edit"
|
|
||||||
onPress={() => { handleViewEditKomentar() }}
|
|
||||||
/>
|
|
||||||
<MenuItemRow
|
|
||||||
icon={<Ionicons name="trash-outline" color={colors.text} size={25} />}
|
|
||||||
title="Hapus"
|
|
||||||
onPress={() => {
|
|
||||||
setVisible(false)
|
|
||||||
setTimeout(() => {
|
|
||||||
setShowDeleteModal(true)
|
|
||||||
}, 600)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
</DrawerBottom>
|
</DrawerBottom>
|
||||||
|
|
||||||
@@ -566,10 +404,7 @@ export default function DiscussionDetail() {
|
|||||||
visible={showDeleteModal}
|
visible={showDeleteModal}
|
||||||
title="Konfirmasi"
|
title="Konfirmasi"
|
||||||
message="Apakah anda yakin ingin menghapus komentar?"
|
message="Apakah anda yakin ingin menghapus komentar?"
|
||||||
onConfirm={() => {
|
onConfirm={() => { setShowDeleteModal(false); handleDeleteKomentar() }}
|
||||||
setShowDeleteModal(false)
|
|
||||||
handleDeleteKomentar()
|
|
||||||
}}
|
|
||||||
onCancel={() => setShowDeleteModal(false)}
|
onCancel={() => setShowDeleteModal(false)}
|
||||||
confirmText="Hapus"
|
confirmText="Hapus"
|
||||||
cancelText="Batal"
|
cancelText="Batal"
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import BorderBottomItem from "@/components/borderBottomItem";
|
|
||||||
import ButtonTab from "@/components/buttonTab";
|
import ButtonTab from "@/components/buttonTab";
|
||||||
import ImageUser from "@/components/imageNew";
|
import ImageUser from "@/components/imageNew";
|
||||||
import InputSearch from "@/components/inputSearch";
|
import InputSearch from "@/components/inputSearch";
|
||||||
import LabelStatus from "@/components/labelStatus";
|
|
||||||
import SkeletonContent from "@/components/skeletonContent";
|
import SkeletonContent from "@/components/skeletonContent";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
import WrapTab from "@/components/wrapTab";
|
import WrapTab from "@/components/wrapTab";
|
||||||
@@ -11,13 +9,12 @@ import Styles from "@/constants/Styles";
|
|||||||
import { apiGetDiscussion, apiGetDivisionOneFeature } from "@/lib/api";
|
import { apiGetDiscussion, apiGetDivisionOneFeature } from "@/lib/api";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import { useTheme } from "@/providers/ThemeProvider";
|
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 { router, useLocalSearchParams } from "expo-router";
|
||||||
import { useEffect, useState } from "react";
|
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";
|
import { useSelector } from "react-redux";
|
||||||
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: string,
|
id: string,
|
||||||
title: string,
|
title: string,
|
||||||
@@ -30,7 +27,6 @@ type Props = {
|
|||||||
isActive: boolean
|
isActive: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default function DiscussionDivision() {
|
export default function DiscussionDivision() {
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
const { id, active } = useLocalSearchParams<{ id: string, active?: string }>()
|
const { id, active } = useLocalSearchParams<{ id: string, active?: string }>()
|
||||||
@@ -51,17 +47,8 @@ export default function DiscussionDivision() {
|
|||||||
async function handleCheckMember() {
|
async function handleCheckMember() {
|
||||||
try {
|
try {
|
||||||
const hasil = await decryptToken(String(token?.current));
|
const hasil = await decryptToken(String(token?.current));
|
||||||
const response = await apiGetDivisionOneFeature({
|
const response = await apiGetDivisionOneFeature({ id, user: hasil, cat: "check-member" });
|
||||||
id,
|
const response2 = await apiGetDivisionOneFeature({ id, user: hasil, cat: "check-admin" });
|
||||||
user: hasil,
|
|
||||||
cat: "check-member",
|
|
||||||
});
|
|
||||||
|
|
||||||
const response2 = await apiGetDivisionOneFeature({
|
|
||||||
id,
|
|
||||||
user: hasil,
|
|
||||||
cat: "check-admin",
|
|
||||||
});
|
|
||||||
setIsMemberDivision(response.data);
|
setIsMemberDivision(response.data);
|
||||||
setIsAdminDivision(response2.data);
|
setIsAdminDivision(response2.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -80,8 +67,6 @@ export default function DiscussionDivision() {
|
|||||||
setData(response.data)
|
setData(response.data)
|
||||||
} else if (thisPage > 1 && response.data.length > 0) {
|
} else if (thisPage > 1 && response.data.length > 0) {
|
||||||
setData([...data, ...response.data])
|
setData([...data, ...response.data])
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
@@ -91,26 +76,15 @@ export default function DiscussionDivision() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => { handleLoad(false, 1) }, [update.data])
|
||||||
handleLoad(false, 1)
|
useEffect(() => { handleLoad(true, 1) }, [status, search])
|
||||||
}, [update.data])
|
useEffect(() => { handleCheckMember() }, [])
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleLoad(true, 1)
|
|
||||||
}, [status, search])
|
|
||||||
|
|
||||||
const loadMoreData = () => {
|
const loadMoreData = () => {
|
||||||
if (waiting) return
|
if (waiting) return
|
||||||
setTimeout(() => {
|
setTimeout(() => { handleLoad(false, page + 1) }, 1000);
|
||||||
handleLoad(false, page + 1)
|
|
||||||
}, 1000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleCheckMember()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
setRefreshing(true)
|
setRefreshing(true)
|
||||||
handleLoad(false, 1)
|
handleLoad(false, 1)
|
||||||
@@ -118,99 +92,100 @@ export default function DiscussionDivision() {
|
|||||||
setRefreshing(false)
|
setRefreshing(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
const getItem = (_data: unknown, index: number): Props => ({
|
const isOpen = (item: Props) => item.status === 1
|
||||||
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,
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[Styles.p15, { flex: 1, backgroundColor: colors.background }]}>
|
<View style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
{
|
{((entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision) && (
|
||||||
((entityUser.role != "user" && entityUser.role != "coadmin") || isAdminDivision) &&
|
<View style={[Styles.ph15, { paddingTop: 12 }]}>
|
||||||
<View>
|
|
||||||
<WrapTab>
|
<WrapTab>
|
||||||
<ButtonTab
|
<ButtonTab
|
||||||
active={status == "false" ? "false" : "true"}
|
active={status == "false" ? "false" : "true"}
|
||||||
value="true"
|
value="true"
|
||||||
onPress={() => { setStatus("true") }}
|
onPress={() => setStatus("true")}
|
||||||
label="Aktif"
|
label="Aktif"
|
||||||
icon={<Feather name="check-circle" color={status == "false" ? colors.dimmed : 'white'} size={20} />}
|
icon={<Feather name="check-circle" color={status == "false" ? colors.dimmed : 'white'} size={20} />}
|
||||||
n={2} />
|
n={2}
|
||||||
|
/>
|
||||||
<ButtonTab
|
<ButtonTab
|
||||||
active={status == "false" ? "false" : "true"}
|
active={status == "false" ? "false" : "true"}
|
||||||
value="false"
|
value="false"
|
||||||
onPress={() => { setStatus("false") }}
|
onPress={() => setStatus("false")}
|
||||||
label="Arsip"
|
label="Arsip"
|
||||||
icon={<AntDesign name="closecircleo" color={status == "true" ? colors.dimmed : 'white'} size={20} />}
|
icon={<AntDesign name="closecircleo" color={status == "true" ? colors.dimmed : 'white'} size={20} />}
|
||||||
n={2} />
|
n={2}
|
||||||
|
/>
|
||||||
</WrapTab>
|
</WrapTab>
|
||||||
<InputSearch onChange={setSearch} />
|
<InputSearch onChange={setSearch} />
|
||||||
</View>
|
</View>
|
||||||
}
|
)}
|
||||||
|
|
||||||
|
<View style={[Styles.flex1, Styles.ph15, { paddingTop: 8 }]}>
|
||||||
|
{loading ? (
|
||||||
|
arrSkeleton.map((_, i) => <SkeletonContent key={i} />)
|
||||||
|
) : data.length === 0 ? (
|
||||||
|
<View style={[Styles.contentItemCenter, Styles.mt30]}>
|
||||||
|
<Feather name="message-circle" size={42} color={colors.icon + '40'} />
|
||||||
|
<Text style={[Styles.mt10, { color: colors.dimmed, fontSize: 14 }]}>
|
||||||
|
Tidak ada diskusi
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<FlatList
|
||||||
|
data={data}
|
||||||
|
keyExtractor={(_, i) => String(i)}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
onEndReached={loadMoreData}
|
||||||
|
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,
|
||||||
|
{ backgroundColor: pressed ? colors.icon + '10' : colors.card, borderColor: colors.icon + '20' }
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<View style={[Styles.rowItemsCenter, Styles.mb08]}>
|
||||||
|
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="xs" />
|
||||||
|
<View style={[Styles.flex1, { marginLeft: 10 }]}>
|
||||||
|
<Text style={[Styles.textDefaultSemiBold, { color: colors.text }]} numberOfLines={1}>
|
||||||
|
{item.user_name}
|
||||||
|
</Text>
|
||||||
|
{status === "true" && (
|
||||||
|
<View style={[Styles.discussionStatusPill, { borderColor: isOpen(item) ? '#10B981' : colors.dimmed + '80' }]}>
|
||||||
|
<Text style={[Styles.discussionStatusText, { color: isOpen(item) ? '#10B981' : colors.dimmed }]}>
|
||||||
|
{isOpen(item) ? 'Buka' : 'Tutup'}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
|
||||||
<View style={[{ flex: 2 }, Styles.mt05]}>
|
{item.desc ? (
|
||||||
{
|
<Text style={[Styles.textMediumNormal, Styles.discussionCardIndent, { color: colors.dimmed, marginBottom: 10 }]} numberOfLines={2}>
|
||||||
loading ?
|
{item.desc}
|
||||||
arrSkeleton.map((item: any, i: number) => {
|
</Text>
|
||||||
return (
|
) : null}
|
||||||
<SkeletonContent key={i} />
|
|
||||||
)
|
<View style={[Styles.rowItemsCenter, Styles.rowSpaceBetween, Styles.discussionCardIndent]}>
|
||||||
})
|
<View style={Styles.rowItemsCenter}>
|
||||||
:
|
<Feather name="message-square" size={14} color={colors.dimmed} />
|
||||||
data.length > 0 ?
|
<Text style={[Styles.discussionCommentText, { color: colors.dimmed }]}>
|
||||||
<VirtualizedList
|
{item.total_komentar} Komentar
|
||||||
data={data}
|
</Text>
|
||||||
getItemCount={() => data.length}
|
</View>
|
||||||
getItem={getItem}
|
<Text style={[Styles.discussionDateText, { color: colors.dimmed }]}>
|
||||||
renderItem={({ item, index }: { item: Props, index: number }) => {
|
{item.createdAt}
|
||||||
return (
|
</Text>
|
||||||
<BorderBottomItem
|
</View>
|
||||||
key={index}
|
</Pressable>
|
||||||
onPress={() => { router.push(`./discussion/${item.id}`) }}
|
)}
|
||||||
borderType="bottom"
|
/>
|
||||||
icon={
|
)}
|
||||||
<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} size="xs" />
|
|
||||||
}
|
|
||||||
title={item.user_name}
|
|
||||||
subtitle={
|
|
||||||
status == "true" ? item.status == 1 ? <LabelStatus category='success' text='BUKA' size="small" /> : <LabelStatus category='error' text='TUTUP' size="small" /> : <></>
|
|
||||||
}
|
|
||||||
rightTopInfo={item.createdAt}
|
|
||||||
desc={item.desc}
|
|
||||||
leftBottomInfo={
|
|
||||||
<View style={[Styles.rowItemsCenter]}>
|
|
||||||
<Ionicons name="chatbox-ellipses-outline" size={18} color={colors.dimmed} style={Styles.mr05} />
|
|
||||||
<Text style={[Styles.textInformation, { color: colors.dimmed }, Styles.mb05]}>Diskusikan</Text>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
rightBottomInfo={item.total_komentar + ' Komentar'}
|
|
||||||
bgColor="transparent"
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
keyExtractor={(item, index) => String(index)}
|
|
||||||
onEndReached={loadMoreData}
|
|
||||||
onEndReachedThreshold={0.5}
|
|
||||||
showsVerticalScrollIndicator={false}
|
|
||||||
refreshControl={
|
|
||||||
<RefreshControl
|
|
||||||
refreshing={refreshing}
|
|
||||||
onRefresh={handleRefresh}
|
|
||||||
tintColor={colors.icon}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
:
|
|
||||||
(<Text style={[Styles.textDefault, Styles.mv10, { textAlign: "center", color: colors.dimmed }]}>Tidak ada diskusi</Text>)
|
|
||||||
}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -171,11 +171,9 @@ export default function EditMember() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function checkForm() {
|
function checkForm() {
|
||||||
if (Object.values(error).some((v) => v == true) || Object.values(data).some((v) => v == "")) {
|
const requiredFields: (keyof Props)[] = ["idPosition", "idUserRole", "nik", "name", "email", "phone", "gender"];
|
||||||
setDisableBtn(true)
|
const hasEmpty = requiredFields.some((key) => data[key] === "");
|
||||||
} else {
|
setDisableBtn(Object.values(error).some((v) => v === true) || hasEmpty);
|
||||||
setDisableBtn(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -1,18 +1,20 @@
|
|||||||
import BorderBottomItemVertical from "@/components/borderBottomItemVertical";
|
import AppHeader from "@/components/AppHeader";
|
||||||
|
import ModalConfirmation from "@/components/ModalConfirmation";
|
||||||
import SkeletonTwoItem from "@/components/skeletonTwoItem";
|
import SkeletonTwoItem from "@/components/skeletonTwoItem";
|
||||||
import Text from "@/components/Text";
|
import Text from "@/components/Text";
|
||||||
import { ColorsStatus } from "@/constants/ColorsStatus";
|
|
||||||
import Styles from "@/constants/Styles";
|
import Styles from "@/constants/Styles";
|
||||||
import { apiGetNotification, apiReadOneNotification } from "@/lib/api";
|
import { apiGetNotification, apiReadAllNotification, apiReadOneNotification } from "@/lib/api";
|
||||||
import { setUpdateNotification } from "@/lib/notificationSlice";
|
import { setUpdateNotification } from "@/lib/notificationSlice";
|
||||||
import { pushToPage } from "@/lib/pushToPage";
|
import { pushToPage } from "@/lib/pushToPage";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import { useTheme } from "@/providers/ThemeProvider";
|
import { useTheme } from "@/providers/ThemeProvider";
|
||||||
import { Feather } from "@expo/vector-icons";
|
import { Feather } from "@expo/vector-icons";
|
||||||
|
import { router, Stack } from "expo-router";
|
||||||
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
|
import { useInfiniteQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
import { RefreshControl, SafeAreaView, View, VirtualizedList } from "react-native";
|
import { FlatList, Pressable, RefreshControl, SafeAreaView, View } from "react-native";
|
||||||
import { useDispatch, useSelector } from "react-redux";
|
import { useDispatch, useSelector } from "react-redux";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id: string
|
id: string
|
||||||
@@ -24,6 +26,22 @@ type Props = {
|
|||||||
createdAt: string
|
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: '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' }
|
||||||
|
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() {
|
export default function Notification() {
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
@@ -31,8 +49,9 @@ export default function Notification() {
|
|||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
const updateNotification = useSelector((state: any) => state.notificationUpdate)
|
const updateNotification = useSelector((state: any) => state.notificationUpdate)
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
|
const [markingAll, setMarkingAll] = useState(false)
|
||||||
|
const [showConfirm, setShowConfirm] = useState(false)
|
||||||
|
|
||||||
// TanStack Query for Notifications with Infinite Scroll
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
fetchNextPage,
|
fetchNextPage,
|
||||||
@@ -55,12 +74,31 @@ export default function Notification() {
|
|||||||
staleTime: 0,
|
staleTime: 0,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Flatten pages into a single data array
|
|
||||||
const flatData = useMemo(() => {
|
const flatData = useMemo(() => {
|
||||||
return data?.pages.flatMap(page => page.data) || [];
|
return data?.pages.flatMap(page => page.data) || [];
|
||||||
}, [data])
|
}, [data])
|
||||||
|
|
||||||
// Refetch when manual update state changes
|
const listData = useMemo<ListRow[]>(() => {
|
||||||
|
const groups: Record<string, Props[]> = {}
|
||||||
|
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(() => {
|
useEffect(() => {
|
||||||
refetch()
|
refetch()
|
||||||
}, [updateNotification, refetch])
|
}, [updateNotification, refetch])
|
||||||
@@ -71,16 +109,26 @@ export default function Notification() {
|
|||||||
setRefreshing(false)
|
setRefreshing(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadMoreData = () => {
|
const hasUnread = flatData.some((item) => !item.isRead)
|
||||||
if (hasNextPage && !isFetchingNextPage) {
|
|
||||||
fetchNextPage()
|
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) {
|
async function handleReadNotification(id: string, category: string, idContent: string) {
|
||||||
try {
|
try {
|
||||||
const hasil = await decryptToken(String(token?.current))
|
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'] })
|
await queryClient.invalidateQueries({ queryKey: ['notifications'] })
|
||||||
pushToPage(category, idContent)
|
pushToPage(category, idContent)
|
||||||
dispatch(setUpdateNotification(!updateNotification))
|
dispatch(setUpdateNotification(!updateNotification))
|
||||||
@@ -89,69 +137,140 @@ 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 (
|
return (
|
||||||
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<View style={[Styles.p15]}>
|
<Stack.Screen
|
||||||
{
|
options={{
|
||||||
isLoading ?
|
header: () => (
|
||||||
arrSkeleton.map((item, index) => {
|
<AppHeader
|
||||||
return (
|
title="Notifikasi"
|
||||||
<SkeletonTwoItem key={index} />
|
showBack={true}
|
||||||
)
|
onPressLeft={() => router.back()}
|
||||||
})
|
right={
|
||||||
:
|
hasUnread ? (
|
||||||
flatData.length > 0 ?
|
<Pressable
|
||||||
<VirtualizedList
|
onPress={() => setShowConfirm(true)}
|
||||||
data={flatData}
|
disabled={markingAll}
|
||||||
getItemCount={() => flatData.length}
|
style={{ opacity: markingAll ? 0.5 : 1, padding: 4 }}
|
||||||
getItem={getItem}
|
>
|
||||||
renderItem={({ item, index }: { item: Props, index: number }) => {
|
<Feather name="check-square" size={22} color="white" />
|
||||||
return (
|
</Pressable>
|
||||||
<BorderBottomItemVertical
|
) : undefined
|
||||||
borderType="bottom"
|
}
|
||||||
icon={
|
/>
|
||||||
<View style={[Styles.iconContent, item.isRead && ColorsStatus.secondary]}>
|
)
|
||||||
<Feather name="bell" size={25} color="black" />
|
}}
|
||||||
</View>
|
/>
|
||||||
}
|
|
||||||
title={item.title}
|
<ModalConfirmation
|
||||||
rightTopInfo={item.createdAt}
|
visible={showConfirm}
|
||||||
desc={item.desc}
|
title="Tandai Semua Dibaca"
|
||||||
textColor={item.isRead ? 'gray' : colors.text}
|
message="Semua notifikasi akan ditandai sebagai telah dibaca."
|
||||||
onPress={() => {
|
confirmText="Tandai"
|
||||||
handleReadNotification(item.id, item.category, item.idContent)
|
cancelText="Batal"
|
||||||
}}
|
onConfirm={() => {
|
||||||
bgColor={'transparent'}
|
setShowConfirm(false)
|
||||||
/>
|
handleReadAll()
|
||||||
)
|
}}
|
||||||
}}
|
onCancel={() => setShowConfirm(false)}
|
||||||
keyExtractor={(item, index) => String(index)}
|
/>
|
||||||
onEndReached={loadMoreData}
|
|
||||||
onEndReachedThreshold={0.5}
|
<View style={[Styles.flex1, Styles.ph15, { paddingTop: 10 }]}>
|
||||||
showsVerticalScrollIndicator={false}
|
{isLoading ? (
|
||||||
refreshControl={
|
[0, 1, 2, 3, 4].map((_, i) => <SkeletonTwoItem key={i} />)
|
||||||
<RefreshControl
|
) : flatData.length === 0 ? (
|
||||||
refreshing={refreshing}
|
<View style={[Styles.contentItemCenter, Styles.mt30]}>
|
||||||
onRefresh={handleRefresh}
|
<Feather name="bell-off" size={42} color={colors.icon + '40'} />
|
||||||
tintColor={colors.icon}
|
<Text style={[Styles.mt10, { color: colors.dimmed, fontSize: 14 }]}>
|
||||||
/>
|
Tidak ada notifikasi
|
||||||
}
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<FlatList
|
||||||
|
data={listData}
|
||||||
|
keyExtractor={(item, index) => String(index)}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
onEndReached={() => {
|
||||||
|
if (hasNextPage && !isFetchingNextPage) fetchNextPage()
|
||||||
|
}}
|
||||||
|
onEndReachedThreshold={0.5}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={refreshing}
|
||||||
|
onRefresh={handleRefresh}
|
||||||
|
tintColor={colors.icon}
|
||||||
/>
|
/>
|
||||||
:
|
}
|
||||||
<Text style={[Styles.textDefault, Styles.textCenter, { color: colors.dimmed }]}>Tidak ada data</Text>
|
renderItem={({ item }) => {
|
||||||
}
|
if (item._type === 'header') {
|
||||||
|
return (
|
||||||
|
<View style={[Styles.rowItemsCenter, { marginTop: 16, marginBottom: 8 }]}>
|
||||||
|
<Text style={{ fontSize: 11, fontWeight: '600', color: colors.dimmed, letterSpacing: 0.6, textTransform: 'uppercase' }}>
|
||||||
|
{item.date}
|
||||||
|
</Text>
|
||||||
|
<View style={{ flex: 1, height: 1, backgroundColor: colors.icon + '20', marginLeft: 8 }} />
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { icon, color } = getNotifStyle(item.category)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Pressable
|
||||||
|
onPress={() => 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 */}
|
||||||
|
<View style={{
|
||||||
|
width: 42,
|
||||||
|
height: 42,
|
||||||
|
borderRadius: 21,
|
||||||
|
backgroundColor: color + '20',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
flexShrink: 0,
|
||||||
|
}}>
|
||||||
|
<Feather name={icon} size={20} color={color} />
|
||||||
|
</View>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<View style={[Styles.flex1, { marginLeft: 10 }]}>
|
||||||
|
<View style={[Styles.rowSpaceBetween, Styles.itemsCenter]}>
|
||||||
|
<View style={[Styles.flex1, Styles.mr10]}>
|
||||||
|
<Text
|
||||||
|
style={[Styles.textDefaultSemiBold, { color: item.isRead ? colors.dimmed : colors.text }]}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
{item.title}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
<Text
|
||||||
|
style={[Styles.textMediumNormal, { color: item.isRead ? colors.dimmed : colors.text, opacity: item.isRead ? 0.7 : 1 }]}
|
||||||
|
numberOfLines={2}
|
||||||
|
>
|
||||||
|
{item.desc}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</Pressable>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,13 +9,12 @@ import Styles from "@/constants/Styles";
|
|||||||
import { apiGetSearch } from "@/lib/api";
|
import { apiGetSearch } from "@/lib/api";
|
||||||
import { useAuthSession } from "@/providers/AuthProvider";
|
import { useAuthSession } from "@/providers/AuthProvider";
|
||||||
import { useTheme } from "@/providers/ThemeProvider";
|
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 { router, Stack } from "expo-router";
|
||||||
import React, { useState } from "react";
|
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";
|
import Toast from "react-native-toast-message";
|
||||||
|
|
||||||
// ... types ...
|
|
||||||
type PropsUser = {
|
type PropsUser = {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
@@ -38,6 +37,27 @@ type PropDivisi = {
|
|||||||
group: string
|
group: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FilterType = "all" | "member" | "division" | "project"
|
||||||
|
|
||||||
|
function SectionHeader({ label, count, colors }: { label: string; count: number; colors: any }) {
|
||||||
|
return (
|
||||||
|
<View style={[Styles.rowItemsCenter, Styles.mb08]}>
|
||||||
|
<Text style={{ fontSize: 11, fontWeight: '600', color: colors.dimmed, letterSpacing: 0.8, textTransform: 'uppercase' }}>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
<View style={{
|
||||||
|
marginLeft: 6,
|
||||||
|
backgroundColor: colors.icon + '25',
|
||||||
|
borderRadius: 10,
|
||||||
|
paddingHorizontal: 7,
|
||||||
|
paddingVertical: 1,
|
||||||
|
}}>
|
||||||
|
<Text style={{ fontSize: 11, color: colors.dimmed, fontWeight: '600' }}>{count}</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export default function Search() {
|
export default function Search() {
|
||||||
const { token, decryptToken } = useAuthSession()
|
const { token, decryptToken } = useAuthSession()
|
||||||
const [dataUser, setDataUser] = useState<PropsUser[]>([])
|
const [dataUser, setDataUser] = useState<PropsUser[]>([])
|
||||||
@@ -45,11 +65,16 @@ export default function Search() {
|
|||||||
const [dataProject, setDataProject] = useState<PropProject[]>([])
|
const [dataProject, setDataProject] = useState<PropProject[]>([])
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
const [refreshing, setRefreshing] = useState(false)
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
|
const [activeFilter, setActiveFilter] = useState<FilterType>("all")
|
||||||
const { colors } = useTheme();
|
const { colors } = useTheme();
|
||||||
|
|
||||||
|
const totalResults = dataUser.length + dataDivisi.length + dataProject.length
|
||||||
|
const hasSearch = search.length >= 3
|
||||||
|
|
||||||
async function handleSearch(cari: string) {
|
async function handleSearch(cari: string) {
|
||||||
try {
|
try {
|
||||||
setSearch(cari)
|
setSearch(cari)
|
||||||
|
setActiveFilter("all")
|
||||||
if (cari.length >= 3) {
|
if (cari.length >= 3) {
|
||||||
const user = await decryptToken(String(token?.current))
|
const user = await decryptToken(String(token?.current))
|
||||||
const hasil = await apiGetSearch({ text: cari, user: user })
|
const hasil = await apiGetSearch({ text: cari, user: user })
|
||||||
@@ -58,7 +83,7 @@ export default function Search() {
|
|||||||
setDataDivisi(hasil.data.division)
|
setDataDivisi(hasil.data.division)
|
||||||
setDataProject(hasil.data.project)
|
setDataProject(hasil.data.project)
|
||||||
} else {
|
} else {
|
||||||
return Toast.show({ type: 'small', text1: hasil.message, })
|
return Toast.show({ type: 'small', text1: hasil.message })
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setDataUser([])
|
setDataUser([])
|
||||||
@@ -68,15 +93,10 @@ export default function Search() {
|
|||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
const message = error?.response?.data?.message || "Gagal melakukan pencarian"
|
const message = error?.response?.data?.message || "Gagal melakukan pencarian"
|
||||||
|
Toast.show({ type: 'small', text1: message })
|
||||||
Toast.show({
|
|
||||||
type: 'small',
|
|
||||||
text1: message
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
const handleRefresh = async () => {
|
||||||
setRefreshing(true)
|
setRefreshing(true)
|
||||||
handleSearch(search)
|
handleSearch(search)
|
||||||
@@ -84,114 +104,203 @@ export default function Search() {
|
|||||||
setRefreshing(false)
|
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 (
|
return (
|
||||||
<>
|
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||||
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
<Stack.Screen
|
||||||
<Stack.Screen
|
options={{
|
||||||
options={{
|
headerTitle: 'Pencarian',
|
||||||
headerTitle: 'Pencarian',
|
headerTitleAlign: 'center',
|
||||||
headerTitleAlign: 'center',
|
header: () => (
|
||||||
header: () => (
|
<AppHeader title="Pencarian" showBack={true} onPressLeft={() => router.back()} />
|
||||||
<AppHeader title="Pencarian" showBack={true} onPressLeft={() => router.back()} />
|
)
|
||||||
)
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
<View style={[Styles.p15]}>
|
<View style={[Styles.flex1]}>
|
||||||
|
{/* Search bar */}
|
||||||
|
<View style={[Styles.ph15, { paddingTop: 15 }]}>
|
||||||
<InputSearch onChange={handleSearch} />
|
<InputSearch onChange={handleSearch} />
|
||||||
{
|
|
||||||
dataProject.length + dataDivisi.length + dataUser.length > 0
|
|
||||||
?
|
|
||||||
<ScrollView
|
|
||||||
style={[Styles.h100]}
|
|
||||||
refreshControl={
|
|
||||||
<RefreshControl
|
|
||||||
refreshing={refreshing}
|
|
||||||
onRefresh={handleRefresh}
|
|
||||||
tintColor={colors.icon}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
dataUser.length > 0 &&
|
|
||||||
<View style={[Styles.mv05, Styles.p10]}>
|
|
||||||
<Text>ANGGOTA</Text>
|
|
||||||
{
|
|
||||||
dataUser.map((item, index) => (
|
|
||||||
<BorderBottomItem
|
|
||||||
key={index}
|
|
||||||
borderType="bottom"
|
|
||||||
icon={<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} />}
|
|
||||||
title={item.name}
|
|
||||||
subtitle={`${item.group}-${item.position}`}
|
|
||||||
onPress={() => {
|
|
||||||
router.push(`/member/${item.id}`)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
dataDivisi.length > 0 &&
|
|
||||||
<View style={[Styles.mv05, Styles.p10]}>
|
|
||||||
<Text>DIVISI</Text>
|
|
||||||
{
|
|
||||||
dataDivisi.map((item, index) => (
|
|
||||||
<BorderBottomItem
|
|
||||||
key={index}
|
|
||||||
borderType="bottom"
|
|
||||||
icon={
|
|
||||||
<View style={[Styles.iconContent, ColorsStatus.primary]}>
|
|
||||||
<MaterialIcons name="group" size={25} color="white" />
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
title={item.name}
|
|
||||||
subtitle={item.group}
|
|
||||||
onPress={() => {
|
|
||||||
router.push(`/division/${item.id}`)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
{
|
|
||||||
dataProject.length > 0 &&
|
|
||||||
<View style={[Styles.mv05, Styles.p10]}>
|
|
||||||
<Text>KEGIATAN</Text>
|
|
||||||
{
|
|
||||||
dataProject.map((item, index) => (
|
|
||||||
<BorderBottomItem
|
|
||||||
key={index}
|
|
||||||
borderType="bottom"
|
|
||||||
icon={
|
|
||||||
<View style={[Styles.iconContent, ColorsStatus.primary]}>
|
|
||||||
<AntDesign name="areachart" size={25} color="white" />
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
title={item.title}
|
|
||||||
subtitle={item.group}
|
|
||||||
onPress={() => {
|
|
||||||
router.push(`/project/${item.id}`)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
</ScrollView>
|
|
||||||
:
|
|
||||||
<View style={[Styles.contentItemCenter, Styles.mt10]}>
|
|
||||||
<Text style={[Styles.textInformation, { color: colors.icon }]}>Tidak ada data</Text>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
|
|
||||||
</View>
|
</View>
|
||||||
</SafeAreaView>
|
|
||||||
</>
|
{/* Filter tabs */}
|
||||||
|
{hasSearch && totalResults > 0 && (
|
||||||
|
<View style={{ marginTop: 10, flexShrink: 0 }}>
|
||||||
|
<ScrollView
|
||||||
|
horizontal
|
||||||
|
showsHorizontalScrollIndicator={false}
|
||||||
|
contentContainerStyle={[Styles.ph15, { paddingRight: 5, alignItems: 'center' }]}
|
||||||
|
>
|
||||||
|
{filters.map((f) => {
|
||||||
|
const isActive = activeFilter === f.key
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={f.key}
|
||||||
|
onPress={() => 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',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{
|
||||||
|
fontSize: 13,
|
||||||
|
color: isActive ? colors.tabActive : colors.dimmed,
|
||||||
|
fontWeight: isActive ? '600' : 'normal',
|
||||||
|
}}>
|
||||||
|
{f.label}
|
||||||
|
</Text>
|
||||||
|
{f.count > 0 && (
|
||||||
|
<View style={{
|
||||||
|
marginLeft: 5,
|
||||||
|
backgroundColor: isActive ? colors.tabActive : colors.icon + '30',
|
||||||
|
borderRadius: 10,
|
||||||
|
minWidth: 18,
|
||||||
|
height: 18,
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
paddingHorizontal: 4,
|
||||||
|
}}>
|
||||||
|
<Text style={{ fontSize: 11, color: isActive ? 'white' : colors.dimmed, fontWeight: '600' }}>
|
||||||
|
{f.count}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</TouchableOpacity>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ScrollView>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<View style={[Styles.flex1]}>
|
||||||
|
{!hasSearch ? (
|
||||||
|
<View style={[Styles.contentItemCenter, Styles.mt30]}>
|
||||||
|
<Feather name="search" size={42} color={colors.icon + '40'} />
|
||||||
|
<Text style={[Styles.mt10, { color: colors.dimmed, fontSize: 14 }]}>
|
||||||
|
Ketik minimal 3 karakter untuk mencari
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : totalResults === 0 ? (
|
||||||
|
<View style={[Styles.contentItemCenter, Styles.mt30]}>
|
||||||
|
<Feather name="inbox" size={42} color={colors.icon + '40'} />
|
||||||
|
<Text style={[Styles.mt10, { color: colors.dimmed, fontSize: 14 }]}>
|
||||||
|
Tidak ada hasil untuk "{search}"
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<ScrollView
|
||||||
|
style={[Styles.flex1]}
|
||||||
|
contentContainerStyle={[Styles.ph15, { paddingTop: 14, paddingBottom: 30 }]}
|
||||||
|
showsVerticalScrollIndicator={false}
|
||||||
|
refreshControl={
|
||||||
|
<RefreshControl
|
||||||
|
refreshing={refreshing}
|
||||||
|
onRefresh={handleRefresh}
|
||||||
|
tintColor={colors.icon}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{/* Anggota */}
|
||||||
|
{showUser && dataUser.length > 0 && (
|
||||||
|
<View style={[Styles.mb15]}>
|
||||||
|
<SectionHeader label="Anggota" count={dataUser.length} colors={colors} />
|
||||||
|
{dataUser.map((item, index) => (
|
||||||
|
<View key={index} style={index < dataUser.length - 1 ? Styles.mb05 : undefined}>
|
||||||
|
<BorderBottomItem
|
||||||
|
borderType="all"
|
||||||
|
icon={<ImageUser src={`${ConstEnv.url_storage}/files/${item.img}`} />}
|
||||||
|
title={item.name}
|
||||||
|
subtitle={`${item.group} · ${item.position}`}
|
||||||
|
onPress={() => router.push(`/member/${item.id}`)}
|
||||||
|
colorPress
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Divisi */}
|
||||||
|
{showDivision && dataDivisi.length > 0 && (
|
||||||
|
<View style={[Styles.mb15]}>
|
||||||
|
<SectionHeader label="Divisi" count={dataDivisi.length} colors={colors} />
|
||||||
|
{dataDivisi.map((item, index) => (
|
||||||
|
<View key={index} style={index < dataDivisi.length - 1 ? Styles.mb05 : undefined}>
|
||||||
|
<BorderBottomItem
|
||||||
|
borderType="all"
|
||||||
|
icon={
|
||||||
|
<View style={[Styles.iconContent, ColorsStatus.primary]}>
|
||||||
|
<MaterialIcons name="group" size={25} color="white" />
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
title={item.name}
|
||||||
|
subtitle={item.group}
|
||||||
|
onPress={() => router.push(`/division/${item.id}`)}
|
||||||
|
colorPress
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Kegiatan */}
|
||||||
|
{showProject && dataProject.length > 0 && (
|
||||||
|
<View style={[Styles.mb15]}>
|
||||||
|
<SectionHeader label="Kegiatan" count={dataProject.length} colors={colors} />
|
||||||
|
{dataProject.map((item, index) => (
|
||||||
|
<View key={index} style={index < dataProject.length - 1 ? Styles.mb05 : undefined}>
|
||||||
|
<BorderBottomItem
|
||||||
|
borderType="all"
|
||||||
|
icon={
|
||||||
|
<View style={[Styles.iconContent, ColorsStatus.primary]}>
|
||||||
|
<AntDesign name="areachart" size={25} color="white" />
|
||||||
|
</View>
|
||||||
|
}
|
||||||
|
title={item.title}
|
||||||
|
subtitle={item.group}
|
||||||
|
onPress={() => router.push(`/project/${item.id}`)}
|
||||||
|
colorPress
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Empty state untuk filter aktif */}
|
||||||
|
{activeFilter !== "all" && activeFilterEmpty && (
|
||||||
|
<View style={[Styles.contentItemCenter, Styles.mt30]}>
|
||||||
|
<Feather name="inbox" size={42} color={colors.icon + '40'} />
|
||||||
|
<Text style={[Styles.mt10, { color: colors.dimmed, fontSize: 14 }]}>
|
||||||
|
Tidak ada hasil di kategori ini
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
</ScrollView>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</SafeAreaView>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1113,6 +1113,62 @@ const Styles = StyleSheet.create({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
marginLeft: 10,
|
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;
|
export default Styles;
|
||||||
@@ -859,6 +859,11 @@ export const apiReadOneNotification = async (data: { user: string, id: string })
|
|||||||
return response.data;
|
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 () => {
|
export const apiGetVersion = async () => {
|
||||||
const response = await api.get(`mobile/version`);
|
const response = await api.get(`mobile/version`);
|
||||||
return response.data;
|
return response.data;
|
||||||
|
|||||||
Reference in New Issue
Block a user