deskripsi: - warna refresh control pada semua fitur - warna bottom pada modal select No Issues
347 lines
14 KiB
TypeScript
347 lines
14 KiB
TypeScript
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 { ConstEnv } from "@/constants/ConstEnv";
|
|
import { isImageFile } from "@/constants/FileExtensions";
|
|
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 * as FileSystem from 'expo-file-system';
|
|
import { startActivityAsync } from 'expo-intent-launcher';
|
|
import { router, Stack, useLocalSearchParams } from "expo-router";
|
|
import * as Sharing from 'expo-sharing';
|
|
import React, { useEffect, useState } from "react";
|
|
import { Dimensions, Platform, Pressable, RefreshControl, SafeAreaView, ScrollView, View } from "react-native";
|
|
import ImageViewing from 'react-native-image-viewing';
|
|
import * as mime from 'react-native-mime-types';
|
|
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;
|
|
desc: string;
|
|
}
|
|
|
|
interface FileData {
|
|
id: string;
|
|
idStorage: string;
|
|
name: string;
|
|
extension: string;
|
|
}
|
|
|
|
interface MemberData {
|
|
group: string;
|
|
division: string;
|
|
}
|
|
|
|
interface ApiResponse {
|
|
success: boolean;
|
|
data: AnnouncementData;
|
|
member: Record<string, MemberData[]>;
|
|
file: FileData[];
|
|
message: string;
|
|
}
|
|
|
|
export default function DetailAnnouncement() {
|
|
const { id } = useLocalSearchParams<{ id: string }>();
|
|
const { token, decryptToken } = useAuthSession()
|
|
const { colors } = useTheme();
|
|
const [data, setData] = useState<AnnouncementData>({ id: '', title: '', desc: '' })
|
|
const [dataMember, setDataMember] = useState<Record<string, MemberData[]>>({})
|
|
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 [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>()
|
|
|
|
/**
|
|
* Opens the image preview modal for the selected image file
|
|
* @param item The file data object containing image information
|
|
*/
|
|
|
|
function handleChooseFile(item: FileData) {
|
|
setChooseFile(item)
|
|
setPreview(true)
|
|
}
|
|
|
|
|
|
async function handleLoad(loading: boolean) {
|
|
try {
|
|
setLoading(loading)
|
|
const hasil = await decryptToken(String(token?.current))
|
|
const response: ApiResponse = await apiGetAnnouncementOne({ id: id, user: hasil })
|
|
if (response.success) {
|
|
setData(response.data)
|
|
setDataMember(response.member)
|
|
setDataFile(response.file)
|
|
} else {
|
|
Toast.show({ type: 'small', text1: response.message })
|
|
}
|
|
} catch (error) {
|
|
console.error(error)
|
|
Toast.show({ type: 'small', text1: 'Gagal mengambil data' })
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
handleLoad(false)
|
|
}, [update])
|
|
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
};
|
|
|
|
const openFile = async (item: FileData) => {
|
|
try {
|
|
setLoadingOpen(true);
|
|
const remoteUrl = ConstEnv.url_storage + '/files/' + item.idStorage;
|
|
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',
|
|
{
|
|
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 (error) {
|
|
console.error('Error downloading or opening file:', error);
|
|
Toast.show({
|
|
type: 'error',
|
|
text1: 'Gagal membuka file',
|
|
text2: 'Silakan coba lagi nanti'
|
|
});
|
|
} finally {
|
|
setLoadingOpen(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.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"
|
|
showBack={true}
|
|
onPressLeft={() => router.back()}
|
|
right={entityUser.role != 'user' && entityUser.role != 'coadmin' ? <HeaderRightAnnouncementDetail id={id} /> : <></>}
|
|
/>
|
|
)
|
|
}}
|
|
/>
|
|
<ScrollView
|
|
showsVerticalScrollIndicator={false}
|
|
style={[Styles.h100, { backgroundColor: colors.background }]}
|
|
refreshControl={
|
|
<RefreshControl
|
|
refreshing={refreshing}
|
|
onRefresh={() => handleRefresh()}
|
|
tintColor={colors.icon}
|
|
/>
|
|
}
|
|
>
|
|
<View style={[Styles.p15, Styles.mb50]}>
|
|
<View style={[Styles.wrapPaper, { backgroundColor: colors.card, borderColor: colors.background }]}>
|
|
{
|
|
loading ?
|
|
<View>
|
|
<View style={[Styles.rowOnly]}>
|
|
<Skeleton width={30} height={30} borderRadius={10} />
|
|
<View style={[{ flex: 1 }, Styles.ph05]}>
|
|
<Skeleton width={100} widthType="percent" height={30} borderRadius={10} />
|
|
</View>
|
|
</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.rowItemsCenter, { alignItems: 'flex-start' }]}>
|
|
<MaterialIcons name="campaign" size={25} color={colors.text} style={[Styles.mr05]} />
|
|
<Text style={[Styles.textDefaultSemiBold, Styles.w90, Styles.mt02]}>{data?.title}</Text>
|
|
</View>
|
|
<View style={[Styles.mt10]}>
|
|
{
|
|
hasHtmlTags(data?.desc) ?
|
|
<RenderHTML
|
|
contentWidth={contentWidth}
|
|
source={{ html: data?.desc }}
|
|
baseStyle={{ color: colors.text }}
|
|
/>
|
|
:
|
|
<Text>{data?.desc}</Text>
|
|
}
|
|
</View>
|
|
</>
|
|
}
|
|
|
|
</View>
|
|
{
|
|
dataFile.length > 0 && (
|
|
<View style={[Styles.wrapPaper, Styles.mt10, { backgroundColor: colors.card, borderColor: colors.background }]}>
|
|
<View style={[Styles.mb05]}>
|
|
<Text style={[Styles.textDefaultSemiBold]}>File</Text>
|
|
</View>
|
|
{dataFile.map((item, index) => (
|
|
<BorderBottomItem
|
|
key={`${item.id}-${index}`}
|
|
borderType="bottom"
|
|
icon={<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)
|
|
}}
|
|
/>
|
|
))}
|
|
</View>
|
|
)
|
|
}
|
|
<View style={[Styles.wrapPaper, Styles.mt10, { backgroundColor: colors.card, borderColor: colors.background }]}>
|
|
{
|
|
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} />
|
|
</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>
|
|
)
|
|
})
|
|
}
|
|
|
|
</View>
|
|
)
|
|
})
|
|
}
|
|
</View>
|
|
</View>
|
|
</ScrollView>
|
|
|
|
<ImageViewing
|
|
images={[{ uri: `${ConstEnv.url_storage}/files/${chooseFile?.idStorage}` }]}
|
|
imageIndex={0}
|
|
visible={preview}
|
|
onRequestClose={() => setPreview(false)}
|
|
doubleTapToZoomEnabled
|
|
HeaderComponent={({ imageIndex }) => (
|
|
<View style={[Styles.headerModalViewImg]}>
|
|
{/* CLOSE */}
|
|
<Pressable
|
|
onPress={() => setPreview(false)}
|
|
accessibilityRole="button"
|
|
accessibilityLabel="Close image viewer"
|
|
>
|
|
<Text style={{ color: 'white', fontSize: 26 }}>✕</Text>
|
|
</Pressable>
|
|
|
|
{/* MENU */}
|
|
<Pressable
|
|
onPress={() => chooseFile && openFile(chooseFile)}
|
|
accessibilityRole="button"
|
|
accessibilityLabel="Download or share image"
|
|
disabled={loadingOpen}
|
|
>
|
|
<Text style={{ color: loadingOpen ? 'gray' : 'white', fontSize: 26 }}>⋯</Text>
|
|
</Pressable>
|
|
</View>
|
|
)}
|
|
FooterComponent={({ imageIndex }) => (
|
|
<View style={{
|
|
paddingBottom: 20,
|
|
paddingHorizontal: 16,
|
|
alignItems: 'center',
|
|
}}>
|
|
<Text style={{ color: 'white', fontSize: 16 }}>{chooseFile?.name}.{chooseFile?.extension}</Text>
|
|
</View>
|
|
)}
|
|
/>
|
|
</SafeAreaView>
|
|
)
|
|
} |