feat: redesign halaman pengumuman dan pindahkan styles ke announcement.styles.ts
- Redesign list, detail, create, dan edit pengumuman menggunakan pola sectionCard - Buat constants/styles/announcement.styles.ts untuk class announcementList* dan announcementDetail* - Hapus local StyleSheet S dari index.tsx dan [id].tsx, ganti dengan Styles global - Tambah getFileIcon/getFileColor helper dan fileGrid berwarna per tipe file - Sesuaikan edit/[id].tsx dengan pola design create.tsx
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import HeaderRightAnnouncementDetail from "@/components/announcement/headerAnnouncementDetail";
|
||||
import AppHeader from "@/components/AppHeader";
|
||||
import BorderBottomItem from "@/components/borderBottomItem";
|
||||
import Skeleton from "@/components/skeleton";
|
||||
import Text from '@/components/Text';
|
||||
import ErrorView from "@/components/ErrorView";
|
||||
@@ -10,7 +9,7 @@ import Styles from "@/constants/Styles";
|
||||
import { apiGetAnnouncementOne } from "@/lib/api";
|
||||
import { useAuthSession } from "@/providers/AuthProvider";
|
||||
import { useTheme } from "@/providers/ThemeProvider";
|
||||
import { Entypo, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
|
||||
import { MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
|
||||
import * as FileSystem from 'expo-file-system';
|
||||
import { startActivityAsync } from 'expo-intent-launcher';
|
||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||
@@ -23,7 +22,6 @@ import RenderHTML from 'react-native-render-html';
|
||||
import Toast from "react-native-toast-message";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
// Define TypeScript interfaces for better type safety
|
||||
interface AnnouncementData {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -59,26 +57,31 @@ export default function DetailAnnouncement() {
|
||||
const [dataFile, setDataFile] = useState<FileData[]>([])
|
||||
const update = useSelector((state: any) => state.announcementUpdate)
|
||||
const entityUser = useSelector((state: any) => state.user)
|
||||
const contentWidth = Dimensions.get('window').width
|
||||
const contentWidth = Dimensions.get('window').width - 62
|
||||
const [loading, setLoading] = useState(true)
|
||||
const arrSkeleton = Array.from({ length: 2 }, (_, index) => index)
|
||||
const [refreshing, setRefreshing] = useState(false)
|
||||
const [loadingOpen, setLoadingOpen] = useState(false)
|
||||
const [preview, setPreview] = useState(false)
|
||||
const [chooseFile, setChooseFile] = useState<FileData>()
|
||||
const [isError, setIsError] = useState(false)
|
||||
|
||||
/**
|
||||
* Opens the image preview modal for the selected image file
|
||||
* @param item The file data object containing image information
|
||||
*/
|
||||
const themed = {
|
||||
background: { backgroundColor: colors.background },
|
||||
card: { backgroundColor: colors.card, borderColor: colors.icon + '18' },
|
||||
iconBox: { backgroundColor: colors.icon + '18' },
|
||||
sectionLabel: { color: colors.dimmed },
|
||||
titleText: { color: colors.text },
|
||||
fileChipBorder: { borderColor: colors.icon + '30' },
|
||||
fileChipPressed: { backgroundColor: colors.icon + '10' },
|
||||
groupSeparator: { borderTopColor: colors.icon + '18' },
|
||||
divisionIconBg: { backgroundColor: colors.icon + '15' },
|
||||
}
|
||||
|
||||
function handleChooseFile(item: FileData) {
|
||||
setChooseFile(item)
|
||||
setPreview(true)
|
||||
}
|
||||
|
||||
|
||||
async function handleLoad(loading: boolean) {
|
||||
try {
|
||||
setIsError(false)
|
||||
@@ -97,39 +100,22 @@ export default function DetailAnnouncement() {
|
||||
console.error(error);
|
||||
setIsError(true)
|
||||
const message = error?.response?.data?.message || "Gagal mengambil data"
|
||||
|
||||
Toast.show({ type: 'small', text1: message })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
handleLoad(false)
|
||||
}, [update])
|
||||
useEffect(() => { handleLoad(false) }, [update])
|
||||
useEffect(() => { handleLoad(true) }, [])
|
||||
|
||||
useEffect(() => {
|
||||
handleLoad(true)
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* Checks if a string contains HTML tags
|
||||
* @param text The text to check for HTML tags
|
||||
* @returns True if the text contains HTML tags, false otherwise
|
||||
*/
|
||||
function hasHtmlTags(text: string) {
|
||||
const htmlRegex = /<[a-z][\s\S]*>/i;
|
||||
return htmlRegex.test(text);
|
||||
return /<[a-z][\s\S]*>/i.test(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles pull-to-refresh functionality
|
||||
* Reloads the announcement data without showing loading indicators
|
||||
*/
|
||||
const handleRefresh = async () => {
|
||||
setRefreshing(true)
|
||||
handleLoad(false)
|
||||
// Simulate network request delay for better UX
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
setRefreshing(false)
|
||||
};
|
||||
@@ -141,178 +127,171 @@ export default function DetailAnnouncement() {
|
||||
const fileName = item.name + '.' + item.extension;
|
||||
const localPath = `${FileSystem.documentDirectory}/${fileName}`;
|
||||
const mimeType = mime.lookup(fileName);
|
||||
|
||||
// Download the file
|
||||
const downloadResult = await FileSystem.downloadAsync(remoteUrl, localPath);
|
||||
|
||||
if (downloadResult.status !== 200) {
|
||||
throw new Error(`Download failed with status ${downloadResult.status}`);
|
||||
}
|
||||
|
||||
const contentURL = await FileSystem.getContentUriAsync(downloadResult.uri);
|
||||
|
||||
try {
|
||||
if (Platform.OS === 'android') {
|
||||
await startActivityAsync(
|
||||
'android.intent.action.VIEW',
|
||||
{
|
||||
await startActivityAsync('android.intent.action.VIEW', {
|
||||
data: contentURL,
|
||||
flags: 1,
|
||||
type: mimeType as string,
|
||||
}
|
||||
);
|
||||
});
|
||||
} else if (Platform.OS === 'ios') {
|
||||
await Sharing.shareAsync(localPath);
|
||||
}
|
||||
} catch (openError) {
|
||||
console.error('Error opening file:', openError);
|
||||
Toast.show({
|
||||
type: 'error',
|
||||
text1: 'Tidak ada aplikasi yang dapat membuka file ini'
|
||||
});
|
||||
} catch {
|
||||
Toast.show({ type: 'error', text1: 'Tidak ada aplikasi yang dapat membuka file ini' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error downloading or opening file:', error);
|
||||
Toast.show({
|
||||
type: 'error',
|
||||
text1: 'Gagal membuka file',
|
||||
text2: 'Silakan coba lagi nanti'
|
||||
});
|
||||
} catch {
|
||||
Toast.show({ type: 'error', text1: 'Gagal membuka file', text2: 'Silakan coba lagi nanti' });
|
||||
} finally {
|
||||
setLoadingOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||
<SafeAreaView style={[Styles.flex1, themed.background]}>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
// headerLeft: () => <ButtonBackHeader onPress={() => { router.back() }} />,
|
||||
headerTitle: 'Pengumuman',
|
||||
headerTitleAlign: 'center',
|
||||
// headerRight: () => entityUser.role != 'user' && entityUser.role != 'coadmin' ? <HeaderRightAnnouncementDetail id={id} /> : <></>,
|
||||
header: () => (
|
||||
<AppHeader title="Pengumuman"
|
||||
<AppHeader
|
||||
title="Pengumuman"
|
||||
showBack={true}
|
||||
onPressLeft={() => router.back()}
|
||||
right={entityUser.role != 'user' && entityUser.role != 'coadmin' ? <HeaderRightAnnouncementDetail id={id} /> : <></>}
|
||||
right={entityUser.role != 'user' && entityUser.role != 'coadmin'
|
||||
? <HeaderRightAnnouncementDetail id={id} />
|
||||
: <></>
|
||||
}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={[Styles.h100, { backgroundColor: colors.background }]}
|
||||
style={[Styles.flex1, themed.background]}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={refreshing}
|
||||
onRefresh={() => handleRefresh()}
|
||||
tintColor={colors.icon}
|
||||
/>
|
||||
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} tintColor={colors.icon} />
|
||||
}
|
||||
>
|
||||
{isError && !loading ? (
|
||||
<View style={[Styles.mv50]}>
|
||||
<View style={Styles.mv50}>
|
||||
<ErrorView />
|
||||
</View>
|
||||
) : (
|
||||
<View style={[Styles.p15, Styles.mb50]}>
|
||||
<View style={[Styles.wrapPaper, Styles.borderAll, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
|
||||
{
|
||||
loading ?
|
||||
<View>
|
||||
<View style={[Styles.rowOnly]}>
|
||||
<Skeleton width={30} height={30} borderRadius={10} />
|
||||
<View style={[Styles.flex1, Styles.ph05]}>
|
||||
<Skeleton width={100} widthType="percent" height={30} borderRadius={10} />
|
||||
<View style={Styles.announcementDetailContainer}>
|
||||
|
||||
{/* Title + Description */}
|
||||
<View style={[Styles.wrapPaper, Styles.noShadow, Styles.sectionCard, Styles.announcementDetailCard, themed.card]}>
|
||||
{loading ? (
|
||||
<View style={Styles.announcementDetailSkeletonGap}>
|
||||
<View style={[Styles.rowItemsCenter, Styles.announcementDetailSkeletonIconRow]}>
|
||||
<Skeleton width={38} height={38} borderRadius={10} />
|
||||
<Skeleton width={60} widthType="percent" height={16} borderRadius={6} />
|
||||
</View>
|
||||
<Skeleton width={100} widthType="percent" height={10} borderRadius={6} />
|
||||
<Skeleton width={100} widthType="percent" height={10} borderRadius={6} />
|
||||
<Skeleton width={80} widthType="percent" height={10} borderRadius={6} />
|
||||
</View>
|
||||
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
||||
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
||||
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
||||
</View>
|
||||
:
|
||||
) : (
|
||||
<>
|
||||
<View style={[Styles.rowOnly, Styles.alignStart]}>
|
||||
<MaterialIcons name="campaign" size={25} color={colors.text} style={[Styles.mr05]} />
|
||||
<Text style={[Styles.textDefaultSemiBold, Styles.w90, Styles.mt02]}>{data?.title}</Text>
|
||||
<View style={[Styles.rowItemsCenter, Styles.announcementDetailTitleRow]}>
|
||||
<View style={[Styles.sectionIconBox, Styles.announcementDetailIconBox, themed.iconBox]}>
|
||||
<MaterialIcons name="campaign" size={22} color={colors.icon} />
|
||||
</View>
|
||||
<View style={[Styles.mt10]}>
|
||||
{
|
||||
hasHtmlTags(data?.desc) ?
|
||||
<RenderHTML
|
||||
<Text style={[Styles.textDefaultSemiBold, Styles.announcementDetailTitleText, themed.titleText]} numberOfLines={2}>
|
||||
{data.title}
|
||||
</Text>
|
||||
</View>
|
||||
{hasHtmlTags(data.desc)
|
||||
? <RenderHTML
|
||||
contentWidth={contentWidth}
|
||||
source={{ html: data?.desc }}
|
||||
source={{ html: data.desc }}
|
||||
baseStyle={{ color: colors.text }}
|
||||
/>
|
||||
:
|
||||
<Text>{data?.desc}</Text>
|
||||
: <Text style={Styles.textDefault}>{data.desc}</Text>
|
||||
}
|
||||
</View>
|
||||
</>
|
||||
}
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Files */}
|
||||
{dataFile.length > 0 && (
|
||||
<View>
|
||||
<View style={[Styles.rowItemsCenter, Styles.announcementDetailSectionLabelRow]}>
|
||||
<MaterialCommunityIcons name="paperclip" size={14} color={colors.dimmed} />
|
||||
<Text style={[Styles.textInformation, themed.sectionLabel]}>
|
||||
Lampiran ({dataFile.length})
|
||||
</Text>
|
||||
</View>
|
||||
{
|
||||
dataFile.length > 0 && (
|
||||
<View style={[Styles.wrapPaper, Styles.borderAll, Styles.mt10, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
|
||||
<View style={[Styles.mb05]}>
|
||||
<Text style={[Styles.textDefaultSemiBold]}>File</Text>
|
||||
</View>
|
||||
<View style={[Styles.wrapPaper, Styles.noShadow, Styles.sectionCard, Styles.announcementDetailCard, Styles.announcementDetailFileCardPadding, themed.card]}>
|
||||
<ScrollView horizontal showsHorizontalScrollIndicator={false}>
|
||||
<View style={[Styles.rowItemsCenter, Styles.announcementDetailFileChipList]}>
|
||||
{dataFile.map((item, index) => (
|
||||
<BorderBottomItem
|
||||
<Pressable
|
||||
key={`${item.id}-${index}`}
|
||||
borderType={index === dataFile.length - 1 ? 'none' : 'bottom'}
|
||||
icon={<MaterialCommunityIcons
|
||||
onPress={() => isImageFile(item.extension) ? handleChooseFile(item) : openFile(item)}
|
||||
style={({ pressed }) => [Styles.announcementDetailFileChip, themed.fileChipBorder,
|
||||
pressed ? themed.fileChipPressed : themed.background]}
|
||||
>
|
||||
<MaterialCommunityIcons
|
||||
name={isImageFile(item.extension) ? "file-image-outline" : "file-document-outline"}
|
||||
size={25}
|
||||
color={colors.text}
|
||||
/>}
|
||||
title={item.name + '.' + item.extension}
|
||||
titleWeight="normal"
|
||||
onPress={() => {
|
||||
isImageFile(item.extension) ?
|
||||
handleChooseFile(item)
|
||||
: openFile(item)
|
||||
}}
|
||||
size={16}
|
||||
color={colors.icon}
|
||||
/>
|
||||
<Text style={[Styles.textInformation, Styles.announcementDetailFileChipText, themed.titleText]} numberOfLines={1}>
|
||||
{item.name}.{item.extension}
|
||||
</Text>
|
||||
</Pressable>
|
||||
))}
|
||||
</View>
|
||||
)
|
||||
}
|
||||
<View style={[Styles.wrapPaper, Styles.borderAll, Styles.mt10, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
|
||||
{
|
||||
loading ?
|
||||
arrSkeleton.map((item, index) => {
|
||||
return (
|
||||
<View key={index}>
|
||||
<Skeleton width={30} widthType="percent" height={10} borderRadius={10} />
|
||||
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
||||
<Skeleton width={100} widthType="percent" height={10} borderRadius={10} />
|
||||
</ScrollView>
|
||||
</View>
|
||||
)
|
||||
})
|
||||
:
|
||||
Object.keys(dataMember).map((v: any, i: any) => {
|
||||
return (
|
||||
<View key={i} style={[Styles.mb05]}>
|
||||
<Text style={[Styles.textDefaultSemiBold]}>{dataMember[v]?.[0].group}</Text>
|
||||
{
|
||||
dataMember[v].map((item: any, x: any) => {
|
||||
return (
|
||||
<View key={x} style={[Styles.rowItemsCenter, Styles.w90]}>
|
||||
<Entypo name="dot-single" size={24} color={colors.text} />
|
||||
<Text style={[Styles.textDefault]} numberOfLines={1} ellipsizeMode='tail'>{item.division}</Text>
|
||||
</View>
|
||||
)
|
||||
})
|
||||
}
|
||||
)}
|
||||
|
||||
{/* Recipients */}
|
||||
<View>
|
||||
<View style={[Styles.rowItemsCenter, Styles.announcementDetailSectionLabelRow]}>
|
||||
<MaterialIcons name="groups" size={14} color={colors.dimmed} />
|
||||
<Text style={[Styles.textInformation, themed.sectionLabel]}>
|
||||
Ditujukan Kepada
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
})
|
||||
}
|
||||
<View style={[Styles.wrapPaper, Styles.noShadow, Styles.sectionCard, Styles.announcementDetailCard, themed.card]}>
|
||||
{loading ? (
|
||||
<View style={Styles.announcementDetailRecipientGap}>
|
||||
<Skeleton width={40} widthType="percent" height={10} borderRadius={6} />
|
||||
<Skeleton width={100} widthType="percent" height={10} borderRadius={6} />
|
||||
<Skeleton width={60} widthType="percent" height={10} borderRadius={6} />
|
||||
<Skeleton width={100} widthType="percent" height={10} borderRadius={6} />
|
||||
</View>
|
||||
) : (
|
||||
Object.keys(dataMember).map((v, i) => (
|
||||
<View key={i} style={i > 0 ? [Styles.announcementDetailGroupSeparator, themed.groupSeparator] : undefined}>
|
||||
<Text style={[Styles.textInformation, Styles.announcementDetailGroupLabel, themed.sectionLabel]}>
|
||||
{dataMember[v]?.[0].group}
|
||||
</Text>
|
||||
<View>
|
||||
{dataMember[v].map((item, x) => (
|
||||
<View key={x} style={[Styles.rowItemsCenter, Styles.announcementDetailDivisionRow]}>
|
||||
<View style={[Styles.announcementDetailDivisionIconCircle, themed.divisionIconBg]}>
|
||||
<MaterialIcons name="group" size={14} color={colors.icon} />
|
||||
</View>
|
||||
<Text style={[Styles.textDefault, Styles.flex1, themed.titleText]}>
|
||||
{item.division}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
|
||||
</View>
|
||||
)}
|
||||
</ScrollView>
|
||||
@@ -323,34 +302,22 @@ export default function DetailAnnouncement() {
|
||||
visible={preview}
|
||||
onRequestClose={() => setPreview(false)}
|
||||
doubleTapToZoomEnabled
|
||||
HeaderComponent={({ imageIndex }) => (
|
||||
<View style={[Styles.headerModalViewImg]}>
|
||||
{/* CLOSE */}
|
||||
<Pressable
|
||||
onPress={() => setPreview(false)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Close image viewer"
|
||||
>
|
||||
HeaderComponent={() => (
|
||||
<View style={Styles.headerModalViewImg}>
|
||||
<Pressable onPress={() => setPreview(false)} accessibilityRole="button">
|
||||
<Text style={[Styles.textWhite, Styles.font26]}>✕</Text>
|
||||
</Pressable>
|
||||
|
||||
{/* MENU */}
|
||||
<Pressable
|
||||
onPress={() => chooseFile && openFile(chooseFile)}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Download or share image"
|
||||
disabled={loadingOpen}
|
||||
>
|
||||
<Text style={[{ color: loadingOpen ? 'gray' : 'white' }, Styles.font26]}>⋯</Text>
|
||||
<Text style={[Styles.font26, { color: loadingOpen ? 'gray' : 'white' }]}>⋯</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
)}
|
||||
FooterComponent={({ imageIndex }) => (
|
||||
<View style={[
|
||||
Styles.pb20,
|
||||
Styles.ph16,
|
||||
Styles.alignCenter,
|
||||
]}>
|
||||
FooterComponent={() => (
|
||||
<View style={[Styles.pb20, Styles.ph16, Styles.alignCenter]}>
|
||||
<Text style={[Styles.textWhite, Styles.font16]}>{chooseFile?.name}.{chooseFile?.extension}</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import AppHeader from "@/components/AppHeader";
|
||||
import BorderBottomItem from "@/components/borderBottomItem";
|
||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||
import ButtonSelect from "@/components/buttonSelect";
|
||||
import DrawerBottom from "@/components/drawerBottom";
|
||||
import { InputForm } from "@/components/inputForm";
|
||||
import LoadingCenter from "@/components/loadingCenter";
|
||||
@@ -13,14 +11,34 @@ import { setUpdateAnnouncement } from "@/lib/announcementUpdate";
|
||||
import { apiCreateAnnouncement } from "@/lib/api";
|
||||
import { useAuthSession } from "@/providers/AuthProvider";
|
||||
import { useTheme } from "@/providers/ThemeProvider";
|
||||
import { Entypo, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import { Ionicons, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
|
||||
import * as DocumentPicker from "expo-document-picker";
|
||||
import { router, Stack } from "expo-router";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { SafeAreaView, ScrollView, View } from "react-native";
|
||||
import { Pressable, SafeAreaView, ScrollView, View } from "react-native";
|
||||
import Toast from "react-native-toast-message";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
function getFileIcon(ext: string): keyof typeof MaterialCommunityIcons.glyphMap {
|
||||
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif'].includes(ext)) return 'image-outline'
|
||||
if (ext === 'pdf') return 'file-pdf-box'
|
||||
if (['mp4', 'mov', 'avi', 'mkv'].includes(ext)) return 'video-outline'
|
||||
if (['doc', 'docx'].includes(ext)) return 'file-word-outline'
|
||||
if (['xls', 'xlsx'].includes(ext)) return 'file-excel-outline'
|
||||
if (['zip', 'rar', '7z'].includes(ext)) return 'zip-box-outline'
|
||||
return 'file-outline'
|
||||
}
|
||||
|
||||
function getFileColor(ext: string): string {
|
||||
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif'].includes(ext)) return '#339AF0'
|
||||
if (ext === 'pdf') return '#F03E3E'
|
||||
if (['mp4', 'mov', 'avi', 'mkv'].includes(ext)) return '#AE3EC9'
|
||||
if (['doc', 'docx'].includes(ext)) return '#1C7ED6'
|
||||
if (['xls', 'xlsx'].includes(ext)) return '#2F9E44'
|
||||
if (['zip', 'rar', '7z'].includes(ext)) return '#E8590C'
|
||||
return '#868E96'
|
||||
}
|
||||
|
||||
export default function CreateAnnouncement() {
|
||||
const dispatch = useDispatch()
|
||||
const update = useSelector((state: any) => state.announcementUpdate)
|
||||
@@ -28,109 +46,70 @@ export default function CreateAnnouncement() {
|
||||
const { colors } = useTheme();
|
||||
const [disableBtn, setDisableBtn] = useState(true);
|
||||
const [modalDivisi, setModalDivisi] = useState(false);
|
||||
const [divisionMember, setDivisionMember] = useState<any>([])
|
||||
const [divisionMember, setDivisionMember] = useState<any[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [fileForm, setFileForm] = useState<any[]>([])
|
||||
const [isModalFile, setModalFile] = useState(false)
|
||||
const [indexDelFile, setIndexDelFile] = useState<number>(0)
|
||||
const [dataForm, setDataForm] = useState({
|
||||
title: "",
|
||||
desc: "",
|
||||
});
|
||||
const [error, setError] = useState({
|
||||
title: false,
|
||||
desc: false,
|
||||
});
|
||||
const [dataForm, setDataForm] = useState({ title: "", desc: "" });
|
||||
const [error, setError] = useState({ title: false, desc: false });
|
||||
|
||||
const totalDivisi = divisionMember.reduce((acc: number, g: any) => acc + g.Division.length, 0)
|
||||
|
||||
function validationForm(cat: string, val: any) {
|
||||
if (cat == "title") {
|
||||
if (cat === "title") {
|
||||
setDataForm({ ...dataForm, title: val });
|
||||
if (val == "" || val == "null") {
|
||||
setError({ ...error, title: true });
|
||||
} else {
|
||||
setError({ ...error, title: false });
|
||||
}
|
||||
} else if (cat == "desc") {
|
||||
setError({ ...error, title: val === "" || val === "null" });
|
||||
} else if (cat === "desc") {
|
||||
setDataForm({ ...dataForm, desc: val });
|
||||
if (val == "" || val == "null") {
|
||||
setError({ ...error, desc: true });
|
||||
} else {
|
||||
setError({ ...error, desc: false });
|
||||
}
|
||||
setError({ ...error, desc: val === "" || val === "null" });
|
||||
}
|
||||
}
|
||||
|
||||
function checkForm() {
|
||||
if (
|
||||
Object.values(error).some((v) => v == true) ||
|
||||
Object.values(dataForm).some((v) => v == "")
|
||||
) {
|
||||
setDisableBtn(true);
|
||||
} else {
|
||||
setDisableBtn(false);
|
||||
}
|
||||
const hasError = Object.values(error).some(v => v)
|
||||
const hasEmpty = Object.values(dataForm).some(v => v === "")
|
||||
setDisableBtn(hasError || hasEmpty);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
checkForm();
|
||||
}, [error, dataForm]);
|
||||
useEffect(() => { checkForm() }, [error, dataForm]);
|
||||
|
||||
async function handleCreate() {
|
||||
try {
|
||||
setLoading(true)
|
||||
const hasil = await decryptToken(String(token?.current))
|
||||
const fd = new FormData()
|
||||
|
||||
for (let i = 0; i < fileForm.length; i++) {
|
||||
fd.append(`file${i}`, {
|
||||
uri: fileForm[i].uri,
|
||||
type: 'application/octet-stream',
|
||||
name: fileForm[i].name,
|
||||
} as any);
|
||||
fd.append(`file${i}`, { uri: fileForm[i].uri, type: 'application/octet-stream', name: fileForm[i].name } as any);
|
||||
}
|
||||
|
||||
fd.append("data", JSON.stringify(
|
||||
{ user: hasil, groups: divisionMember, ...dataForm }
|
||||
))
|
||||
|
||||
fd.append("data", JSON.stringify({ user: hasil, groups: divisionMember, ...dataForm }))
|
||||
const response = await apiCreateAnnouncement(fd)
|
||||
|
||||
if (response.success) {
|
||||
dispatch(setUpdateAnnouncement(!update))
|
||||
Toast.show({ type: 'small', text1: 'Berhasil menambahkan data', })
|
||||
Toast.show({ type: 'small', text1: 'Berhasil menambahkan data' })
|
||||
router.back();
|
||||
} else {
|
||||
Toast.show({ type: 'small', text1: response.message, })
|
||||
Toast.show({ type: 'small', text1: response.message })
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
const message = error?.response?.data?.message || "Tidak dapat terhubung ke server"
|
||||
|
||||
Toast.show({
|
||||
type: 'small',
|
||||
text1: message
|
||||
})
|
||||
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Tidak dapat terhubung ke server" })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const pickDocumentAsync = async () => {
|
||||
let result = await DocumentPicker.getDocumentAsync({
|
||||
type: ["*/*"],
|
||||
multiple: true
|
||||
});
|
||||
const result = await DocumentPicker.getDocumentAsync({ type: ["*/*"], multiple: true });
|
||||
if (!result.canceled) {
|
||||
for (let i = 0; i < result.assets?.length; i++) {
|
||||
if (result.assets[i].uri) {
|
||||
setFileForm((prev) => [...prev, result.assets[i]])
|
||||
}
|
||||
for (const asset of result.assets) {
|
||||
if (asset.uri) setFileForm(prev => [...prev, asset])
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function deleteFile(index: number) {
|
||||
setFileForm([...fileForm.filter((val, i) => i !== index)])
|
||||
setFileForm(fileForm.filter((_, i) => i !== index))
|
||||
setModalFile(false)
|
||||
}
|
||||
|
||||
@@ -138,26 +117,6 @@ export default function CreateAnnouncement() {
|
||||
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
// headerLeft: () => (
|
||||
// <ButtonBackHeader
|
||||
// onPress={() => {
|
||||
// router.back();
|
||||
// }}
|
||||
// />
|
||||
// ),
|
||||
headerTitle: "Tambah Pengumuman",
|
||||
headerTitleAlign: "center",
|
||||
// headerRight: () => (
|
||||
// <ButtonSaveHeader
|
||||
// disable={disableBtn || divisionMember.length == 0 || loading ? true : false}
|
||||
// category="create"
|
||||
// onPress={() => {
|
||||
// divisionMember.length == 0
|
||||
// ? Toast.show({ type: 'small', text1: "Anda belum memilih divisi", })
|
||||
// : handleCreate();
|
||||
// }}
|
||||
// />
|
||||
// ),
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Tambah Pengumuman"
|
||||
@@ -165,12 +124,12 @@ export default function CreateAnnouncement() {
|
||||
onPressLeft={() => router.back()}
|
||||
right={
|
||||
<ButtonSaveHeader
|
||||
disable={disableBtn || divisionMember.length == 0 || loading ? true : false}
|
||||
disable={disableBtn || divisionMember.length === 0 || loading}
|
||||
category="create"
|
||||
onPress={() => {
|
||||
divisionMember.length == 0
|
||||
? Toast.show({ type: 'small', text1: "Anda belum memilih divisi", })
|
||||
: handleCreate();
|
||||
divisionMember.length === 0
|
||||
? Toast.show({ type: 'small', text1: "Anda belum memilih divisi" })
|
||||
: handleCreate()
|
||||
}}
|
||||
/>
|
||||
}
|
||||
@@ -179,11 +138,9 @@ export default function CreateAnnouncement() {
|
||||
}}
|
||||
/>
|
||||
{loading && <LoadingCenter />}
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={[Styles.h100, { backgroundColor: colors.background }]}
|
||||
>
|
||||
<View style={[Styles.p15]}>
|
||||
<ScrollView showsVerticalScrollIndicator={false} style={[Styles.h100, { backgroundColor: colors.background }]}>
|
||||
<View style={Styles.p15}>
|
||||
|
||||
<InputForm
|
||||
label="Judul"
|
||||
type="default"
|
||||
@@ -194,6 +151,7 @@ export default function CreateAnnouncement() {
|
||||
errorText="Judul harus diisi"
|
||||
onChange={(val) => validationForm("title", val)}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label="Pengumuman"
|
||||
type="default"
|
||||
@@ -205,68 +163,105 @@ export default function CreateAnnouncement() {
|
||||
onChange={(val) => validationForm("desc", val)}
|
||||
multiline
|
||||
/>
|
||||
<ButtonSelect value="Upload File" onPress={pickDocumentAsync} />
|
||||
{
|
||||
fileForm.length > 0
|
||||
&&
|
||||
<>
|
||||
<View style={[Styles.rowSpaceBetween, Styles.mv05]}>
|
||||
<Text style={[Styles.textDefaultSemiBold]}>File</Text>
|
||||
<Text style={[Styles.textDefault]}>{fileForm.length} file</Text>
|
||||
</View>
|
||||
<View style={[Styles.borderAll, Styles.round05, Styles.p10, Styles.mb10, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
|
||||
{
|
||||
fileForm.map((item, index) => (
|
||||
<BorderBottomItem
|
||||
key={index}
|
||||
borderType={fileForm.length - 1 == index ? "none" : "bottom"}
|
||||
icon={<MaterialCommunityIcons name="file-outline" size={25} color={colors.text} />}
|
||||
title={item.name}
|
||||
bgColor="transparent"
|
||||
titleWeight="normal"
|
||||
onPress={() => { setIndexDelFile(index); setModalFile(true) }}
|
||||
/>
|
||||
))
|
||||
}
|
||||
</View>
|
||||
</>
|
||||
}
|
||||
|
||||
<ButtonSelect
|
||||
value="Pilih divisi penerima pengumuman"
|
||||
onPress={() => {
|
||||
setModalDivisi(true)
|
||||
}}
|
||||
/>
|
||||
|
||||
{
|
||||
divisionMember.length > 0
|
||||
&&
|
||||
<>
|
||||
<View style={[Styles.rowSpaceBetween, Styles.mv05]}>
|
||||
<Text style={[Styles.textDefaultSemiBold]}>Divisi</Text>
|
||||
{/* File */}
|
||||
<View style={[Styles.wrapPaper, Styles.mb15, Styles.sectionCard,
|
||||
{ backgroundColor: colors.card, borderColor: colors.icon + '18' }]}>
|
||||
<Pressable
|
||||
onPress={pickDocumentAsync}
|
||||
style={[Styles.sectionActionRow, { marginBottom: fileForm.length > 0 ? 12 : 0 }]}
|
||||
>
|
||||
<View style={[Styles.sectionIconBox, { backgroundColor: colors.icon + '15' }]}>
|
||||
<MaterialCommunityIcons name="paperclip" size={18} color={colors.dimmed} />
|
||||
</View>
|
||||
<View style={[Styles.borderAll, Styles.round05, Styles.p10, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
|
||||
{
|
||||
divisionMember.map((item: { name: any; Division: any }, index: any) => {
|
||||
<View style={Styles.flex1}>
|
||||
<Text style={[Styles.textDefaultSemiBold, { color: colors.text }]}>File</Text>
|
||||
{fileForm.length === 0 && (
|
||||
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]}>Opsional — ketuk untuk upload</Text>
|
||||
)}
|
||||
</View>
|
||||
{fileForm.length > 0 && (
|
||||
<View style={[Styles.sectionBadge, { backgroundColor: colors.dimmed + '18' }]}>
|
||||
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>{fileForm.length} file</Text>
|
||||
</View>
|
||||
)}
|
||||
<MaterialCommunityIcons name="chevron-right" size={18} color={colors.dimmed} />
|
||||
</Pressable>
|
||||
{fileForm.length > 0 && (
|
||||
<View style={Styles.fileGrid}>
|
||||
{fileForm.map((item, index) => {
|
||||
const ext = item.name.split('.').pop()?.toLowerCase() ?? ''
|
||||
const baseName = item.name.includes('.') ? item.name.split('.').slice(0, -1).join('.') : item.name
|
||||
const iconName = getFileIcon(ext)
|
||||
const iconColor = getFileColor(ext)
|
||||
return (
|
||||
<View key={index}>
|
||||
<Text style={[Styles.textDefaultSemiBold]}>{item.name}</Text>
|
||||
{
|
||||
item.Division.map((division: any, i: any) => (
|
||||
<View key={i} style={[Styles.rowItemsCenter, Styles.w90]}>
|
||||
<Entypo name="dot-single" size={24} color={colors.text} />
|
||||
<Text style={[Styles.textDefault]} numberOfLines={1} ellipsizeMode='tail'>{division.name}</Text>
|
||||
<Pressable
|
||||
key={index}
|
||||
onPress={() => { setIndexDelFile(index); setModalFile(true) }}
|
||||
style={[Styles.fileCard, { backgroundColor: 'transparent', borderColor: colors.icon + '18' }]}
|
||||
>
|
||||
<View style={[Styles.sectionIconBox, { backgroundColor: iconColor + '20' }]}>
|
||||
<MaterialCommunityIcons name={iconName} size={18} color={iconColor} />
|
||||
</View>
|
||||
))
|
||||
}
|
||||
<View style={Styles.flex1}>
|
||||
<Text style={Styles.textDefault} numberOfLines={1}>{baseName}</Text>
|
||||
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>{ext.toUpperCase()}</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
)
|
||||
})
|
||||
}
|
||||
})}
|
||||
</View>
|
||||
</>
|
||||
}
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Divisi Penerima */}
|
||||
<View style={[Styles.wrapPaper, Styles.mb15, Styles.sectionCard,
|
||||
{ backgroundColor: colors.card, borderColor: colors.icon + '18' }]}>
|
||||
<Pressable
|
||||
onPress={() => setModalDivisi(true)}
|
||||
style={[Styles.sectionActionRow, { marginBottom: divisionMember.length > 0 ? 12 : 0 }]}
|
||||
>
|
||||
<View style={[Styles.sectionIconBox, { backgroundColor: colors.tabActive + '18' }]}>
|
||||
<MaterialIcons name="groups" size={18} color={colors.tabActive} />
|
||||
</View>
|
||||
<View style={Styles.flex1}>
|
||||
<Text style={[Styles.textDefaultSemiBold, { color: colors.text }]}>Divisi Penerima</Text>
|
||||
{divisionMember.length === 0 && (
|
||||
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]}>Belum ada divisi dipilih</Text>
|
||||
)}
|
||||
</View>
|
||||
{divisionMember.length > 0 && (
|
||||
<View style={[Styles.sectionBadge, { backgroundColor: colors.tabActive + '18' }]}>
|
||||
<Text style={[Styles.textSmallSemiBold, { color: colors.tabActive }]}>{totalDivisi} divisi</Text>
|
||||
</View>
|
||||
)}
|
||||
<MaterialCommunityIcons name="chevron-right" size={18} color={colors.dimmed} />
|
||||
</Pressable>
|
||||
{divisionMember.length > 0 && (
|
||||
<View style={{ gap: 10 }}>
|
||||
{divisionMember.map((item: any, index: number) => (
|
||||
<View key={index}>
|
||||
<Text style={[Styles.textMediumNormal, { color: colors.dimmed, marginBottom: 4 }]}>
|
||||
{item.name}
|
||||
</Text>
|
||||
<View style={{ gap: 6 }}>
|
||||
{item.Division.map((division: any, i: number) => (
|
||||
<View key={i} style={[Styles.listItemCard, { borderColor: colors.icon + '18' }]}>
|
||||
<View style={[Styles.sectionIconBox, { backgroundColor: colors.tabActive + '18', width: 28, height: 28, borderRadius: 8 }]}>
|
||||
<MaterialIcons name="group" size={14} color={colors.tabActive} />
|
||||
</View>
|
||||
<Text style={[Styles.textDefault, Styles.flex1, { color: colors.text }]} numberOfLines={1}>
|
||||
{division.name}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
@@ -287,12 +282,10 @@ export default function CreateAnnouncement() {
|
||||
<MenuItemRow
|
||||
icon={<Ionicons name="trash-outline" color={colors.text} size={25} />}
|
||||
title="Hapus"
|
||||
onPress={() => { deleteFile(indexDelFile) }}
|
||||
onPress={() => deleteFile(indexDelFile)}
|
||||
/>
|
||||
</View>
|
||||
</DrawerBottom>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import AppHeader from "@/components/AppHeader";
|
||||
import BorderBottomItem from "@/components/borderBottomItem";
|
||||
import ButtonSaveHeader from "@/components/buttonSaveHeader";
|
||||
import ButtonSelect from "@/components/buttonSelect";
|
||||
import DrawerBottom from "@/components/drawerBottom";
|
||||
import { InputForm } from "@/components/inputForm";
|
||||
import LoadingCenter from "@/components/loadingCenter";
|
||||
@@ -13,22 +11,38 @@ import { setUpdateAnnouncement } from "@/lib/announcementUpdate";
|
||||
import { apiEditAnnouncement, apiGetAnnouncementOne } from "@/lib/api";
|
||||
import { useAuthSession } from "@/providers/AuthProvider";
|
||||
import { useTheme } from "@/providers/ThemeProvider";
|
||||
import { Entypo, Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
|
||||
import { Ionicons, MaterialCommunityIcons, MaterialIcons } from "@expo/vector-icons";
|
||||
import * as DocumentPicker from "expo-document-picker";
|
||||
import { router, Stack, useLocalSearchParams } from "expo-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { SafeAreaView, ScrollView, View } from "react-native";
|
||||
import { Pressable, SafeAreaView, ScrollView, View } from "react-native";
|
||||
import Toast from "react-native-toast-message";
|
||||
import { useDispatch, useSelector } from "react-redux";
|
||||
|
||||
function getFileIcon(ext: string): keyof typeof MaterialCommunityIcons.glyphMap {
|
||||
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif'].includes(ext)) return 'image-outline'
|
||||
if (ext === 'pdf') return 'file-pdf-box'
|
||||
if (['mp4', 'mov', 'avi', 'mkv'].includes(ext)) return 'video-outline'
|
||||
if (['doc', 'docx'].includes(ext)) return 'file-word-outline'
|
||||
if (['xls', 'xlsx'].includes(ext)) return 'file-excel-outline'
|
||||
if (['zip', 'rar', '7z'].includes(ext)) return 'zip-box-outline'
|
||||
return 'file-outline'
|
||||
}
|
||||
|
||||
function getFileColor(ext: string): string {
|
||||
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'heic', 'heif'].includes(ext)) return '#339AF0'
|
||||
if (ext === 'pdf') return '#F03E3E'
|
||||
if (['mp4', 'mov', 'avi', 'mkv'].includes(ext)) return '#AE3EC9'
|
||||
if (['doc', 'docx'].includes(ext)) return '#1C7ED6'
|
||||
if (['xls', 'xlsx'].includes(ext)) return '#2F9E44'
|
||||
if (['zip', 'rar', '7z'].includes(ext)) return '#E8590C'
|
||||
return '#868E96'
|
||||
}
|
||||
|
||||
type GroupDivision = {
|
||||
id: string;
|
||||
name: string;
|
||||
Division: {
|
||||
id: string;
|
||||
name: string;
|
||||
}[];
|
||||
Division: { id: string; name: string }[];
|
||||
}
|
||||
|
||||
export default function EditAnnouncement() {
|
||||
@@ -39,43 +53,29 @@ export default function EditAnnouncement() {
|
||||
const { colors } = useTheme();
|
||||
const [modalDivisi, setModalDivisi] = useState(false);
|
||||
const [disableBtn, setDisableBtn] = useState(true);
|
||||
const [dataMember, setDataMember] = useState<any>([]);
|
||||
const [dataMember, setDataMember] = useState<GroupDivision[]>([]);
|
||||
const [fileForm, setFileForm] = useState<any[]>([])
|
||||
const [dataFile, setDataFile] = useState<{ id: string; idStorage: string; name: string; extension: string; delete?: boolean }[]>([])
|
||||
const [indexDelFile, setIndexDelFile] = useState<{ id: string | number; cat: "newFile" | "oldFile" }>({ id: "", cat: "newFile" })
|
||||
const [isModalFile, setModalFile] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [dataForm, setDataForm] = useState({
|
||||
title: "",
|
||||
desc: "",
|
||||
});
|
||||
const [error, setError] = useState({
|
||||
title: false,
|
||||
desc: false,
|
||||
});
|
||||
const [dataForm, setDataForm] = useState({ title: "", desc: "" });
|
||||
const [error, setError] = useState({ title: false, desc: false });
|
||||
|
||||
const visibleOldFiles = dataFile.filter(v => !v.delete)
|
||||
const totalFiles = fileForm.length + visibleOldFiles.length
|
||||
const totalDivisi = dataMember.reduce((acc: number, g: any) => acc + g.Division.length, 0)
|
||||
|
||||
async function handleLoad() {
|
||||
try {
|
||||
const hasil = await decryptToken(String(token?.current));
|
||||
const response = await apiGetAnnouncementOne({ id: id, user: hasil });
|
||||
setDataForm(response.data);
|
||||
|
||||
const arrNew: GroupDivision[] = []
|
||||
const coba = Object.keys(response.member).map((v: any, i: any) => {
|
||||
const newObject = {
|
||||
"id": response.member[v][0].idGroup,
|
||||
"name": v,
|
||||
"Division": response.member[v]
|
||||
}
|
||||
|
||||
response.member[v].map((v: any, i: any) => {
|
||||
newObject["Division"][i] = {
|
||||
"id": v.idDivision,
|
||||
"name": v.division
|
||||
}
|
||||
})
|
||||
arrNew.push(newObject)
|
||||
})
|
||||
const arrNew: GroupDivision[] = Object.keys(response.member).map((v) => ({
|
||||
id: response.member[v][0].idGroup,
|
||||
name: v,
|
||||
Division: response.member[v].map((m: any) => ({ id: m.idDivision, name: m.division }))
|
||||
}))
|
||||
setDataMember(arrNew);
|
||||
setDataFile(response.file);
|
||||
} catch (error) {
|
||||
@@ -83,42 +83,25 @@ export default function EditAnnouncement() {
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
handleLoad();
|
||||
}, []);
|
||||
useEffect(() => { handleLoad() }, []);
|
||||
|
||||
function validationForm(cat: string, val: any) {
|
||||
if (cat == "title") {
|
||||
if (cat === "title") {
|
||||
setDataForm({ ...dataForm, title: val });
|
||||
if (val == "" || val == "null") {
|
||||
setError({ ...error, title: true });
|
||||
} else {
|
||||
setError({ ...error, title: false });
|
||||
}
|
||||
} else if (cat == "desc") {
|
||||
setError({ ...error, title: val === "" || val === "null" });
|
||||
} else if (cat === "desc") {
|
||||
setDataForm({ ...dataForm, desc: val });
|
||||
if (val == "" || val == "null") {
|
||||
setError({ ...error, desc: true });
|
||||
} else {
|
||||
setError({ ...error, desc: false });
|
||||
}
|
||||
setError({ ...error, desc: val === "" || val === "null" });
|
||||
}
|
||||
}
|
||||
|
||||
function checkForm() {
|
||||
if (
|
||||
Object.values(error).some((v) => v == true) ||
|
||||
Object.values(dataForm).some((v) => v == "")
|
||||
) {
|
||||
setDisableBtn(true);
|
||||
} else {
|
||||
setDisableBtn(false);
|
||||
}
|
||||
const hasError = Object.values(error).some(v => v)
|
||||
const hasEmpty = Object.values(dataForm).some(v => v === "")
|
||||
setDisableBtn(hasError || hasEmpty);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
checkForm();
|
||||
}, [error, dataForm]);
|
||||
useEffect(() => { checkForm() }, [error, dataForm]);
|
||||
|
||||
async function handleEdit() {
|
||||
try {
|
||||
@@ -126,90 +109,47 @@ export default function EditAnnouncement() {
|
||||
const hasil = await decryptToken(String(token?.current))
|
||||
const fd = new FormData()
|
||||
for (let i = 0; i < fileForm.length; i++) {
|
||||
fd.append(`file${i}`, {
|
||||
uri: fileForm[i].uri,
|
||||
type: 'application/octet-stream',
|
||||
name: fileForm[i].name,
|
||||
} as any);
|
||||
fd.append(`file${i}`, { uri: fileForm[i].uri, type: 'application/octet-stream', name: fileForm[i].name } as any);
|
||||
}
|
||||
|
||||
fd.append("data", JSON.stringify(
|
||||
{
|
||||
...dataForm, user: hasil, groups: dataMember, oldFile: dataFile
|
||||
}
|
||||
))
|
||||
|
||||
fd.append("data", JSON.stringify({ ...dataForm, user: hasil, groups: dataMember, oldFile: dataFile }))
|
||||
const response = await apiEditAnnouncement(fd, id);
|
||||
if (response.success) {
|
||||
dispatch(setUpdateAnnouncement(!update))
|
||||
Toast.show({ type: 'small', text1: 'Berhasil mengubah data', })
|
||||
Toast.show({ type: 'small', text1: 'Berhasil mengubah data' })
|
||||
router.back();
|
||||
} else {
|
||||
Toast.show({ type: 'small', text1: 'Gagal mengubah data', })
|
||||
Toast.show({ type: 'small', text1: 'Gagal mengubah data' })
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error);
|
||||
const message = error?.response?.data?.message || "Gagal mengubah data"
|
||||
|
||||
Toast.show({ type: 'small', text1: message })
|
||||
Toast.show({ type: 'small', text1: error?.response?.data?.message || "Gagal mengubah data" })
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const pickDocumentAsync = async () => {
|
||||
let result = await DocumentPicker.getDocumentAsync({
|
||||
type: ["*/*"],
|
||||
multiple: true
|
||||
});
|
||||
const result = await DocumentPicker.getDocumentAsync({ type: ["*/*"], multiple: true });
|
||||
if (!result.canceled) {
|
||||
for (let i = 0; i < result.assets?.length; i++) {
|
||||
if (result.assets[i].uri) {
|
||||
setFileForm((prev) => [...prev, result.assets[i]])
|
||||
}
|
||||
for (const asset of result.assets) {
|
||||
if (asset.uri) setFileForm(prev => [...prev, asset])
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function deleteFile(index: number | string, cat: "newFile" | "oldFile" | null) {
|
||||
if (cat == "newFile") {
|
||||
setFileForm([...fileForm.filter((val, i) => i !== index)])
|
||||
if (cat === "newFile") {
|
||||
setFileForm(fileForm.filter((_, i) => i !== index))
|
||||
} else {
|
||||
setDataFile(prev =>
|
||||
prev.map(item =>
|
||||
item.id === index
|
||||
? { ...item, delete: true }
|
||||
: item
|
||||
)
|
||||
);
|
||||
setDataFile(prev => prev.map(item => item.id === index ? { ...item, delete: true } : item))
|
||||
}
|
||||
setModalFile(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1, backgroundColor: colors.background }}>
|
||||
<SafeAreaView style={[Styles.flex1, { backgroundColor: colors.background }]}>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
// headerLeft: () => (
|
||||
// <ButtonBackHeader
|
||||
// onPress={() => {
|
||||
// router.back();
|
||||
// }}
|
||||
// />
|
||||
// ),
|
||||
headerTitle: "Edit Pengumuman",
|
||||
headerTitleAlign: "center",
|
||||
// headerRight: () => (
|
||||
// <ButtonSaveHeader
|
||||
// disable={disableBtn || loading ? true : false}
|
||||
// category="update"
|
||||
// onPress={() => {
|
||||
// dataMember.length == 0
|
||||
// ? Toast.show({ type: 'small', text1: "Anda belum memilih divisi", })
|
||||
// : handleEdit();
|
||||
// }}
|
||||
// />
|
||||
// ),
|
||||
header: () => (
|
||||
<AppHeader
|
||||
title="Edit Pengumuman"
|
||||
@@ -217,12 +157,12 @@ export default function EditAnnouncement() {
|
||||
onPressLeft={() => router.back()}
|
||||
right={
|
||||
<ButtonSaveHeader
|
||||
disable={disableBtn || loading ? true : false}
|
||||
disable={disableBtn || dataMember.length === 0 || loading}
|
||||
category="update"
|
||||
onPress={() => {
|
||||
dataMember.length == 0
|
||||
? Toast.show({ type: 'small', text1: "Anda belum memilih divisi", })
|
||||
: handleEdit();
|
||||
dataMember.length === 0
|
||||
? Toast.show({ type: 'small', text1: "Anda belum memilih divisi" })
|
||||
: handleEdit()
|
||||
}}
|
||||
/>
|
||||
}
|
||||
@@ -231,11 +171,9 @@ export default function EditAnnouncement() {
|
||||
}}
|
||||
/>
|
||||
{loading && <LoadingCenter />}
|
||||
<ScrollView
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={[Styles.h100, { backgroundColor: colors.background }]}
|
||||
>
|
||||
<View style={[Styles.p15]}>
|
||||
<ScrollView showsVerticalScrollIndicator={false} style={[Styles.h100, { backgroundColor: colors.background }]}>
|
||||
<View style={Styles.p15}>
|
||||
|
||||
<InputForm
|
||||
label="Judul"
|
||||
type="default"
|
||||
@@ -247,6 +185,7 @@ export default function EditAnnouncement() {
|
||||
onChange={(val) => validationForm("title", val)}
|
||||
value={dataForm.title}
|
||||
/>
|
||||
|
||||
<InputForm
|
||||
label="Pengumuman"
|
||||
type="default"
|
||||
@@ -259,79 +198,125 @@ export default function EditAnnouncement() {
|
||||
value={dataForm.desc}
|
||||
multiline
|
||||
/>
|
||||
<ButtonSelect value="Upload File" onPress={pickDocumentAsync} />
|
||||
{
|
||||
(fileForm.length > 0 || dataFile.filter((val) => !val.delete).length > 0)
|
||||
&&
|
||||
<>
|
||||
<View style={[Styles.rowSpaceBetween, Styles.mv05]}>
|
||||
<Text style={[Styles.textDefaultSemiBold]}>File</Text>
|
||||
<Text style={[Styles.textDefault]}>{fileForm.length + dataFile.filter((val) => !val.delete).length} file</Text>
|
||||
|
||||
{/* File */}
|
||||
<View style={[Styles.wrapPaper, Styles.mb15, Styles.sectionCard,
|
||||
{ backgroundColor: colors.card, borderColor: colors.icon + '18' }]}>
|
||||
<Pressable
|
||||
onPress={pickDocumentAsync}
|
||||
style={[Styles.sectionActionRow, { marginBottom: totalFiles > 0 ? 12 : 0 }]}
|
||||
>
|
||||
<View style={[Styles.sectionIconBox, { backgroundColor: colors.icon + '15' }]}>
|
||||
<MaterialCommunityIcons name="paperclip" size={18} color={colors.dimmed} />
|
||||
</View>
|
||||
<View style={[Styles.borderAll, Styles.round05, Styles.p10, Styles.mb10, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
|
||||
{
|
||||
dataFile.filter((val) => !val.delete).map((item, index) => (
|
||||
<BorderBottomItem
|
||||
key={index}
|
||||
borderType={dataFile.filter((val) => !val.delete).length - 1 == index && fileForm.length == 0 ? "none" : "bottom"}
|
||||
icon={<MaterialCommunityIcons name="file-outline" size={25} color={colors.text} />}
|
||||
title={item.name + '.' + item.extension}
|
||||
titleWeight="normal"
|
||||
bgColor="transparent"
|
||||
onPress={() => { setIndexDelFile({ id: item.id, cat: "oldFile" }); setModalFile(true) }}
|
||||
/>
|
||||
))
|
||||
}
|
||||
{
|
||||
fileForm.map((item, index) => (
|
||||
<BorderBottomItem
|
||||
key={index}
|
||||
borderType={fileForm.length - 1 == index ? "none" : "bottom"}
|
||||
icon={<MaterialCommunityIcons name="file-outline" size={25} color={colors.text} />}
|
||||
title={item.name}
|
||||
titleWeight="normal"
|
||||
bgColor="transparent"
|
||||
onPress={() => { setIndexDelFile({ id: index, cat: "newFile" }); setModalFile(true) }}
|
||||
/>
|
||||
))
|
||||
}
|
||||
<View style={Styles.flex1}>
|
||||
<Text style={[Styles.textDefaultSemiBold, { color: colors.text }]}>File</Text>
|
||||
{totalFiles === 0 && (
|
||||
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]}>Opsional — ketuk untuk upload</Text>
|
||||
)}
|
||||
</View>
|
||||
</>
|
||||
}
|
||||
<ButtonSelect
|
||||
value="Pilih divisi penerima pengumuman"
|
||||
onPress={() => {
|
||||
setModalDivisi(true)
|
||||
}}
|
||||
/>
|
||||
{
|
||||
dataMember.length > 0
|
||||
&&
|
||||
<>
|
||||
<View style={[Styles.rowSpaceBetween, Styles.mv05]}>
|
||||
<Text style={[Styles.textDefaultSemiBold]}>Divisi</Text>
|
||||
{totalFiles > 0 && (
|
||||
<View style={[Styles.sectionBadge, { backgroundColor: colors.dimmed + '18' }]}>
|
||||
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>{totalFiles} file</Text>
|
||||
</View>
|
||||
<View style={[Styles.borderAll, Styles.round05, Styles.p10, { backgroundColor: colors.card, borderColor: colors.icon + '20' }]}>
|
||||
{
|
||||
dataMember.map((item: { name: any; Division: any }, index: any) => {
|
||||
)}
|
||||
<MaterialCommunityIcons name="chevron-right" size={18} color={colors.dimmed} />
|
||||
</Pressable>
|
||||
{totalFiles > 0 && (
|
||||
<View style={Styles.fileGrid}>
|
||||
{visibleOldFiles.map((item, index) => {
|
||||
const ext = item.extension.toLowerCase()
|
||||
const iconName = getFileIcon(ext)
|
||||
const iconColor = getFileColor(ext)
|
||||
return (
|
||||
<View key={index}>
|
||||
<Text style={[Styles.textDefaultSemiBold]}>{item.name}</Text>
|
||||
{
|
||||
item.Division.map((division: any, i: any) => (
|
||||
<View key={i} style={[Styles.rowItemsCenter, Styles.w90]}>
|
||||
<Entypo name="dot-single" size={24} color={colors.text} />
|
||||
<Text style={[Styles.textDefault]} numberOfLines={1} ellipsizeMode='tail'>{division.name}</Text>
|
||||
<Pressable
|
||||
key={`old-${index}`}
|
||||
onPress={() => { setIndexDelFile({ id: item.id, cat: "oldFile" }); setModalFile(true) }}
|
||||
style={[Styles.fileCard, { backgroundColor: 'transparent', borderColor: colors.icon + '18' }]}
|
||||
>
|
||||
<View style={[Styles.sectionIconBox, { backgroundColor: iconColor + '20' }]}>
|
||||
<MaterialCommunityIcons name={iconName} size={18} color={iconColor} />
|
||||
</View>
|
||||
))
|
||||
}
|
||||
<View style={Styles.flex1}>
|
||||
<Text style={Styles.textDefault} numberOfLines={1}>{item.name}</Text>
|
||||
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>{ext.toUpperCase()}</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
)
|
||||
})
|
||||
}
|
||||
})}
|
||||
{fileForm.map((item, index) => {
|
||||
const ext = item.name.split('.').pop()?.toLowerCase() ?? ''
|
||||
const baseName = item.name.includes('.') ? item.name.split('.').slice(0, -1).join('.') : item.name
|
||||
const iconName = getFileIcon(ext)
|
||||
const iconColor = getFileColor(ext)
|
||||
return (
|
||||
<Pressable
|
||||
key={`new-${index}`}
|
||||
onPress={() => { setIndexDelFile({ id: index, cat: "newFile" }); setModalFile(true) }}
|
||||
style={[Styles.fileCard, { backgroundColor: 'transparent', borderColor: colors.icon + '18' }]}
|
||||
>
|
||||
<View style={[Styles.sectionIconBox, { backgroundColor: iconColor + '20' }]}>
|
||||
<MaterialCommunityIcons name={iconName} size={18} color={iconColor} />
|
||||
</View>
|
||||
</>
|
||||
}
|
||||
<View style={Styles.flex1}>
|
||||
<Text style={Styles.textDefault} numberOfLines={1}>{baseName}</Text>
|
||||
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>{ext.toUpperCase()}</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
)
|
||||
})}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Divisi Penerima */}
|
||||
<View style={[Styles.wrapPaper, Styles.mb15, Styles.sectionCard,
|
||||
{ backgroundColor: colors.card, borderColor: colors.icon + '18' }]}>
|
||||
<Pressable
|
||||
onPress={() => setModalDivisi(true)}
|
||||
style={[Styles.sectionActionRow, { marginBottom: dataMember.length > 0 ? 12 : 0 }]}
|
||||
>
|
||||
<View style={[Styles.sectionIconBox, { backgroundColor: colors.tabActive + '18' }]}>
|
||||
<MaterialIcons name="groups" size={18} color={colors.tabActive} />
|
||||
</View>
|
||||
<View style={Styles.flex1}>
|
||||
<Text style={[Styles.textDefaultSemiBold, { color: colors.text }]}>Divisi Penerima</Text>
|
||||
{dataMember.length === 0 && (
|
||||
<Text style={[Styles.textMediumNormal, { color: colors.dimmed }]}>Belum ada divisi dipilih</Text>
|
||||
)}
|
||||
</View>
|
||||
{dataMember.length > 0 && (
|
||||
<View style={[Styles.sectionBadge, { backgroundColor: colors.tabActive + '18' }]}>
|
||||
<Text style={[Styles.textSmallSemiBold, { color: colors.tabActive }]}>{totalDivisi} divisi</Text>
|
||||
</View>
|
||||
)}
|
||||
<MaterialCommunityIcons name="chevron-right" size={18} color={colors.dimmed} />
|
||||
</Pressable>
|
||||
{dataMember.length > 0 && (
|
||||
<View style={{ gap: 10 }}>
|
||||
{dataMember.map((item, index) => (
|
||||
<View key={index}>
|
||||
<Text style={[Styles.textMediumNormal, { color: colors.dimmed, marginBottom: 4 }]}>
|
||||
{item.name}
|
||||
</Text>
|
||||
<View style={{ gap: 6 }}>
|
||||
{item.Division.map((division, i) => (
|
||||
<View key={i} style={[Styles.listItemCard, { borderColor: colors.icon + '18' }]}>
|
||||
<View style={[Styles.sectionIconBox, { backgroundColor: colors.tabActive + '18', width: 28, height: 28, borderRadius: 8 }]}>
|
||||
<MaterialIcons name="group" size={14} color={colors.tabActive} />
|
||||
</View>
|
||||
<Text style={[Styles.textDefault, Styles.flex1, { color: colors.text }]} numberOfLines={1}>
|
||||
{division.name}
|
||||
</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
</View>
|
||||
</ScrollView>
|
||||
|
||||
@@ -353,7 +338,7 @@ export default function EditAnnouncement() {
|
||||
<MenuItemRow
|
||||
icon={<Ionicons name="trash-outline" color={colors.text} size={25} />}
|
||||
title="Hapus"
|
||||
onPress={() => { deleteFile(indexDelFile.id, indexDelFile.cat) }}
|
||||
onPress={() => deleteFile(indexDelFile.id, indexDelFile.cat)}
|
||||
/>
|
||||
</View>
|
||||
</DrawerBottom>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import GuideOverlay from "@/components/GuideOverlay";
|
||||
import BorderBottomItem from "@/components/borderBottomItem";
|
||||
import InputSearch from "@/components/inputSearch";
|
||||
import SkeletonContent from "@/components/skeletonContent";
|
||||
import Skeleton from "@/components/skeleton";
|
||||
import Text from '@/components/Text';
|
||||
import Styles from "@/constants/Styles";
|
||||
import { apiGetAnnouncement } from "@/lib/api";
|
||||
@@ -13,13 +12,13 @@ import { MaterialIcons } from "@expo/vector-icons";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { router } from "expo-router";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { RefreshControl, View, VirtualizedList } from "react-native";
|
||||
import { Pressable, RefreshControl, View, VirtualizedList } from "react-native";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
type Props = {
|
||||
id: string,
|
||||
title: string,
|
||||
desc: string,
|
||||
id: string
|
||||
title: string
|
||||
desc: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
@@ -29,9 +28,18 @@ export default function Announcement() {
|
||||
const [search, setSearch] = useState('')
|
||||
const update = useSelector((state: any) => state.announcementUpdate)
|
||||
const { visible: guideVisible, dismiss: dismissGuide } = useGuide('announcement')
|
||||
const arrSkeleton = Array.from({ length: 5 }, (_, index) => index)
|
||||
const arrSkeleton = Array.from({ length: 5 }, (_, i) => i)
|
||||
|
||||
const themed = {
|
||||
background: { backgroundColor: colors.background },
|
||||
card: { backgroundColor: colors.card, borderColor: colors.icon + '18' },
|
||||
iconBox: { backgroundColor: colors.icon + '18' },
|
||||
title: { color: colors.text },
|
||||
desc: { color: colors.dimmed },
|
||||
date: { color: colors.dimmed },
|
||||
cardPressed: { backgroundColor: colors.icon + '08' },
|
||||
}
|
||||
|
||||
// TanStack Query Infinite Query
|
||||
const {
|
||||
data,
|
||||
fetchNextPage,
|
||||
@@ -44,11 +52,7 @@ export default function Announcement() {
|
||||
queryKey: ['announcements', search],
|
||||
queryFn: async ({ pageParam = 1 }) => {
|
||||
const hasil = await decryptToken(String(token?.current))
|
||||
const response = await apiGetAnnouncement({
|
||||
user: hasil,
|
||||
search: search,
|
||||
page: pageParam
|
||||
})
|
||||
const response = await apiGetAnnouncement({ user: hasil, search, page: pageParam })
|
||||
return response.data
|
||||
},
|
||||
initialPageParam: 1,
|
||||
@@ -57,21 +61,9 @@ export default function Announcement() {
|
||||
},
|
||||
})
|
||||
|
||||
// Trigger refetch when Redux state 'update' changes
|
||||
useEffect(() => {
|
||||
refetch()
|
||||
}, [update, refetch])
|
||||
useEffect(() => { refetch() }, [update, refetch])
|
||||
|
||||
// Flatten data from pages
|
||||
const flattenedData = useMemo(() => {
|
||||
return data?.pages.flat() || []
|
||||
}, [data])
|
||||
|
||||
const loadMoreData = () => {
|
||||
if (hasNextPage && !isFetchingNextPage) {
|
||||
fetchNextPage()
|
||||
}
|
||||
};
|
||||
const flattenedData = useMemo(() => data?.pages.flat() || [], [data])
|
||||
|
||||
const getItem = (_data: unknown, index: number): Props => ({
|
||||
id: flattenedData[index].id,
|
||||
@@ -80,47 +72,66 @@ export default function Announcement() {
|
||||
createdAt: flattenedData[index].createdAt,
|
||||
})
|
||||
|
||||
return (
|
||||
<View style={[Styles.p15, Styles.flex1, { backgroundColor: colors.background }]}>
|
||||
<GuideOverlay visible={guideVisible} steps={GUIDE_ANNOUNCEMENT} onDismiss={dismissGuide} />
|
||||
<View>
|
||||
<InputSearch onChange={setSearch} />
|
||||
const renderSkeleton = () => (
|
||||
<View style={Styles.announcementListSkeletonCard}>
|
||||
<View style={[Styles.rowSpaceBetween, Styles.rowItemsCenter, Styles.announcementListSkeletonHeader]}>
|
||||
<View style={[Styles.rowItemsCenter, Styles.announcementListSkeletonTitleRow]}>
|
||||
<Skeleton width={28} height={28} borderRadius={8} />
|
||||
<Skeleton width={50} widthType="percent" height={12} borderRadius={6} />
|
||||
</View>
|
||||
<Skeleton width={15} widthType="percent" height={10} borderRadius={6} />
|
||||
</View>
|
||||
<Skeleton width={100} widthType="percent" height={10} borderRadius={6} />
|
||||
<Skeleton width={80} widthType="percent" height={10} borderRadius={6} />
|
||||
</View>
|
||||
<View style={[Styles.flex2, Styles.mt05]}>
|
||||
{
|
||||
isLoading && !flattenedData.length ?
|
||||
arrSkeleton.map((item, index) => {
|
||||
return (
|
||||
<SkeletonContent key={index} />
|
||||
)
|
||||
})
|
||||
:
|
||||
flattenedData.length > 0
|
||||
?
|
||||
|
||||
const renderItem = ({ item }: { item: Props }) => (
|
||||
<Pressable
|
||||
onPress={() => router.push(`/announcement/${item.id}`)}
|
||||
style={({ pressed }) => [Styles.announcementListCard, themed.card, pressed && themed.cardPressed]}
|
||||
>
|
||||
<View style={[Styles.rowSpaceBetween, Styles.rowItemsCenter, Styles.announcementListCardHeader]}>
|
||||
<View style={[Styles.rowItemsCenter, Styles.announcementListTitleRow]}>
|
||||
<View style={[Styles.announcementListIconBox, themed.iconBox]}>
|
||||
<MaterialIcons name="campaign" size={16} color={colors.icon} />
|
||||
</View>
|
||||
<Text style={[Styles.textDefaultSemiBold, Styles.announcementListTitleText, themed.title]} numberOfLines={1}>
|
||||
{item.title}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={[Styles.textInformation, Styles.announcementListDateText, themed.date]}>
|
||||
{item.createdAt}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={[Styles.textMediumNormal, Styles.announcementListDescText, themed.title]} numberOfLines={2}>
|
||||
{item.desc.replace(/<[^>]*>?/gm, '').replace(/\r?\n|\r/g, ' ')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
)
|
||||
|
||||
return (
|
||||
<View style={[Styles.flex1, Styles.announcementListContainer, themed.background]}>
|
||||
<GuideOverlay visible={guideVisible} steps={GUIDE_ANNOUNCEMENT} onDismiss={dismissGuide} />
|
||||
<InputSearch onChange={setSearch} />
|
||||
<View style={[Styles.flex1, Styles.announcementListInner]}>
|
||||
{isLoading && !flattenedData.length ? (
|
||||
arrSkeleton.map((_, i) => (
|
||||
<View key={i} style={[Styles.announcementListCard, themed.card]}>
|
||||
{renderSkeleton()}
|
||||
</View>
|
||||
))
|
||||
) : flattenedData.length > 0 ? (
|
||||
<VirtualizedList
|
||||
data={flattenedData}
|
||||
getItemCount={() => flattenedData.length}
|
||||
getItem={getItem}
|
||||
renderItem={({ item, index }: { item: Props, index: number }) => {
|
||||
return (
|
||||
<BorderBottomItem
|
||||
key={index}
|
||||
onPress={() => { router.push(`/announcement/${item.id}`) }}
|
||||
borderType="bottom"
|
||||
bgColor="transparent"
|
||||
icon={
|
||||
<MaterialIcons name="campaign" size={25} color={colors.text} />
|
||||
}
|
||||
title={item.title}
|
||||
desc={item.desc.replace(/<[^>]*>?/gm, '').replace(/\r?\n|\r/g, ' ')}
|
||||
rightTopInfo={item.createdAt}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={(item, index) => String(item.id || index)}
|
||||
onEndReached={loadMoreData}
|
||||
onEndReached={() => { if (hasNextPage && !isFetchingNextPage) fetchNextPage() }}
|
||||
onEndReachedThreshold={0.5}
|
||||
showsVerticalScrollIndicator={false}
|
||||
ItemSeparatorComponent={() => <View style={Styles.announcementListSeparator} />}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={isRefetching && !isFetchingNextPage}
|
||||
@@ -129,9 +140,11 @@ export default function Announcement() {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
:
|
||||
<Text style={[Styles.textDefault, Styles.textCenter, { color: colors.dimmed }]}>Tidak ada pengumuman</Text>
|
||||
}
|
||||
) : (
|
||||
<Text style={[Styles.textDefault, Styles.textCenter, Styles.mt30, themed.desc]}>
|
||||
Tidak ada pengumuman
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
|
||||
39
constants/styles/announcement.styles.ts
Normal file
39
constants/styles/announcement.styles.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { StyleSheet } from "react-native";
|
||||
|
||||
const AnnouncementStyles = StyleSheet.create({
|
||||
// list (index.tsx)
|
||||
announcementListContainer: { padding: 15, paddingBottom: 0 },
|
||||
announcementListInner: { marginTop: 10 },
|
||||
announcementListCard: { borderRadius: 10, borderWidth: 1, padding: 12 },
|
||||
announcementListCardHeader: { marginBottom: 6 },
|
||||
announcementListTitleRow: { flex: 1, gap: 8, marginRight: 8, flexDirection: 'row', alignItems: 'center' },
|
||||
announcementListIconBox: { width: 28, height: 28, borderRadius: 8, alignItems: 'center', justifyContent: 'center' },
|
||||
announcementListTitleText: { flex: 1 },
|
||||
announcementListDateText: { flexShrink: 0 },
|
||||
announcementListDescText: { lineHeight: 20 },
|
||||
announcementListSeparator: { height: 8 },
|
||||
announcementListSkeletonCard: { gap: 8 },
|
||||
announcementListSkeletonHeader: { marginBottom: 4 },
|
||||
announcementListSkeletonTitleRow: { gap: 8, flex: 1, flexDirection: 'row', alignItems: 'center' },
|
||||
|
||||
// detail ([id].tsx)
|
||||
announcementDetailContainer: { padding: 15, paddingBottom: 50, gap: 12 },
|
||||
announcementDetailCard: { borderRadius: 10 },
|
||||
announcementDetailSkeletonGap: { gap: 8 },
|
||||
announcementDetailSkeletonIconRow: { gap: 10, marginBottom: 2 },
|
||||
announcementDetailTitleRow: { gap: 10, marginBottom: 10 },
|
||||
announcementDetailIconBox: { width: 38, height: 38, borderRadius: 10 },
|
||||
announcementDetailTitleText: { fontSize: 17, lineHeight: 24, flex: 1 },
|
||||
announcementDetailSectionLabelRow: { marginBottom: 8, gap: 6 },
|
||||
announcementDetailFileCardPadding: { padding: 10 },
|
||||
announcementDetailFileChipList: { gap: 8 },
|
||||
announcementDetailFileChip: { flexDirection: 'row', alignItems: 'center', gap: 6, paddingHorizontal: 12, paddingVertical: 8, borderRadius: 20, borderWidth: 1 },
|
||||
announcementDetailFileChipText: { maxWidth: 120 },
|
||||
announcementDetailRecipientGap: { gap: 10 },
|
||||
announcementDetailGroupSeparator: { marginTop: 12, paddingTop: 12, borderTopWidth: 1 },
|
||||
announcementDetailGroupLabel: { marginBottom: 6 },
|
||||
announcementDetailDivisionRow: { gap: 8, paddingVertical: 6 },
|
||||
announcementDetailDivisionIconCircle: { width: 26, height: 26, borderRadius: 13, alignItems: 'center', justifyContent: 'center' },
|
||||
});
|
||||
|
||||
export default AnnouncementStyles;
|
||||
@@ -37,8 +37,8 @@ const ButtonStyles = StyleSheet.create({
|
||||
padding: 5,
|
||||
borderWidth: 1,
|
||||
},
|
||||
labelStatus: { paddingHorizontal: 15, borderRadius: 10 },
|
||||
labelStatusSmall: { paddingHorizontal: 10, borderRadius: 10 },
|
||||
labelStatus: { paddingHorizontal: 15, paddingVertical: 4, borderRadius: 10 },
|
||||
labelStatusSmall: { paddingHorizontal: 10, paddingVertical: 3, borderRadius: 10 },
|
||||
});
|
||||
|
||||
export default ButtonStyles;
|
||||
|
||||
@@ -11,6 +11,7 @@ import HeaderStyles from './header.styles';
|
||||
import ComponentStyles from './component.styles';
|
||||
import NotificationStyles from './notification.styles';
|
||||
import ApprovalStyles from './approval.styles';
|
||||
import AnnouncementStyles from './announcement.styles';
|
||||
|
||||
const Styles = StyleSheet.create({
|
||||
...SpacingStyles,
|
||||
@@ -25,6 +26,7 @@ const Styles = StyleSheet.create({
|
||||
...ComponentStyles,
|
||||
...NotificationStyles,
|
||||
...ApprovalStyles,
|
||||
...AnnouncementStyles,
|
||||
});
|
||||
|
||||
export default Styles;
|
||||
|
||||
Reference in New Issue
Block a user