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:
2026-05-19 14:27:29 +08:00
parent 2bacc47d75
commit 0cb085caa8
7 changed files with 609 additions and 610 deletions

View File

@@ -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"
<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>
{totalFiles > 0 && (
<View style={[Styles.sectionBadge, { backgroundColor: colors.dimmed + '18' }]}>
<Text style={[Styles.textSmallSemiBold, { color: colors.dimmed }]}>{totalFiles} file</Text>
</View>
)}
<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 (
<Pressable
key={`old-${index}`}
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>
</>
}
<ButtonSelect
value="Pilih divisi penerima pengumuman"
onPress={() => {
setModalDivisi(true)
}}
/>
{
dataMember.length > 0
&&
<>
<View style={[Styles.rowSpaceBetween, Styles.mv05]}>
<Text style={[Styles.textDefaultSemiBold]}>Divisi</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) => {
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>
</View>
))
}
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>