Compare commits

...

2 Commits

Author SHA1 Message Date
5c931b069c Fixed navbar admin
User & Admin Layout
- app/(application)/(user)/home.tsx
- app/(application)/admin/_layout.tsx

Components
- components/Drawer/NavbarMenu.tsx
- components/index.ts

Docs
- docs/prompt-for-qwen-code.md

Backup Component
- components/Drawer/NavbarMenu.back.tsx

New Components
- components/Drawer/NavbarMenu_V2.tsx
- components/_ShareComponent/BasicWrapper.tsx

New Admin Screen
- screens/Admin/listPageAdmin_V2.tsx

### No Issue
2026-02-11 17:40:08 +08:00
b2be7be533 Saya telah melakukan serangkaian perubahan penting dalam pengembangan aplikasi HIPMI Mobile, khususnya dalam modul
Donasi. Berikut adalah ringkasan perubahan yang telah dilakukan:

     1. Menerapkan sistem pagination pada berbagai komponen layar donasi:
        - ScreenBeranda.tsx
        - ScreenMyDonation.tsx
        - ScreenRecapOfNews.tsx
        - ScreenListOfNews.tsx
        - ScreenListOfDonatur.tsx
        - ScreenFundDisbursement.tsx

     2. Memperbarui fungsi-fungsi API untuk mendukung parameter page:
        - apiDonationGetAll
        - apiDonationGetNewsById
        - apiDonationListOfDonaturById
        - apiDonationDisbursementOfFundsListById

     3. Mengganti komponen wrapper lama (ViewWrapper) dengan NewWrapper yang mendukung sistem pagination

     4. Membuat komponen layar terpisah untuk meningkatkan modularitas kode

     5. Memperbaiki berbagai error yang terjadi, termasuk masalah dengan import komponen dan struktur JSX

### No Issue
2026-02-10 17:30:30 +08:00
32 changed files with 2498 additions and 776 deletions

View File

@@ -1,56 +1,9 @@
import {
FloatingButton,
LoaderCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import Donation_BoxPublish from "@/screens/Donation/BoxPublish";
import { apiDonationGetAll } from "@/service/api-client/api-donation";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import Donation_ScreenBeranda from "@/screens/Donation/ScreenBeranda";
export default function DonationBeranda() {
const [list, setList] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [])
);
const onLoadData = async () => {
try {
setLoadList(true);
const response = await apiDonationGetAll({
category: "beranda"
});
setList(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadList(false);
}
};
return (
<ViewWrapper
hideFooter
floatingButton={
<FloatingButton onPress={() => router.push("/donation/create")} />
}
>
{loadList ? (
<LoaderCustom />
) : _.isEmpty(list) ? (
<TextCustom align="center" color="gray">Belum ada donasi</TextCustom>
) : (
list?.map((item: any, index: number) => (
<Donation_BoxPublish data={item} key={index} id={item.id} />
))
)}
</ViewWrapper>
<>
<Donation_ScreenBeranda />
</>
);
}

View File

@@ -1,148 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BadgeCustom,
BaseBox,
DummyLandscapeImage,
Grid,
LoaderCustom,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { apiDonationGetAll } from "@/service/api-client/api-donation";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import { Href, router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { View } from "react-native";
import Toast from "react-native-toast-message";
import Donation_ScreenMyDonation from "@/screens/Donation/ScreenMyDonation";
export default function DonationMyDonation() {
const { user } = useAuth();
const [list, setList] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [user?.id]),
);
const onLoadData = async () => {
if (!user?.id) {
Toast.show({
type: "error",
text1: "Load data gagal, user tidak ditemukan",
});
return;
}
try {
setLoadList(true);
const response = await apiDonationGetAll({
category: "my-donation",
authorId: user?.id,
});
setList(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadList(false);
}
};
const handlerColor = (status: string) => {
if (status === "menunggu") {
return "orange";
} else if (status === "proses") {
return "white";
} else if (status === "berhasil") {
return "green";
} else if (status === "gagal") {
return "red";
}
};
const handlePress = ({
invoiceId,
donationId,
status,
}: {
invoiceId: string;
donationId: string;
status: string;
}) => {
const url: Href = `../${donationId}/(transaction-flow)/${invoiceId}`;
if (status === "menunggu") {
router.push(`${url}/invoice`);
} else if (status === "proses") {
router.push(`${url}/process`);
} else if (status === "berhasil") {
router.push(`${url}/success`);
} else if (status === "gagal") {
router.push(`${url}/failed`);
}
};
return (
<ViewWrapper hideFooter>
{loadList ? (
<LoaderCustom />
) : _.isEmpty(list) ? (
<TextCustom align="center" color="gray">
Belum ada transaksi
</TextCustom>
) : (
list?.map((item, index) => (
<BaseBox
key={index}
paddingTop={7}
paddingBottom={7}
onPress={() => {
handlePress({
status: _.lowerCase(item.statusInvoice),
invoiceId: item.id,
donationId: item.donasiId,
});
}}
>
<Grid>
<Grid.Col span={5}>
<DummyLandscapeImage
height={100}
unClickPath
imageId={item.imageId}
/>
</Grid.Col>
<Grid.Col span={1}>
<View />
</Grid.Col>
<Grid.Col span={6}>
<StackCustom>
<TextCustom truncate={2} bold>
{item.title || "-"}
</TextCustom>
<TextCustom bold color="yellow">
Rp. {formatCurrencyDisplay(item.nominal)}
</TextCustom>
<BadgeCustom
variant="light"
color={handlerColor(_.lowerCase(item.statusInvoice))}
fullWidth
>
{item.statusInvoice}
</BadgeCustom>
</StackCustom>
</Grid.Col>
</Grid>
</BaseBox>
))
)}
</ViewWrapper>
);
return <Donation_ScreenMyDonation />;
}

View File

@@ -1,5 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
InformationBox,
@@ -31,7 +32,7 @@ export default function DonationEditNews() {
useFocusEffect(
useCallback(() => {
onLoadData();
}, [news])
}, [news]),
);
const onLoadData = async () => {
@@ -104,7 +105,21 @@ export default function DonationEditNews() {
};
return (
<ViewWrapper>
<ViewWrapper
footerComponent={
<BoxButtonOnFooter>
<ButtonCustom
disabled={!data?.title || !data?.deskripsi}
isLoading={isLoading}
onPress={() => {
handlerSubmitUpdate();
}}
>
Update
</ButtonCustom>
</BoxButtonOnFooter>
}
>
<StackCustom gap={"xs"}>
<InformationBox text="Upload gambar bersifat opsional untuk melengkapi kabar terkait donasi Anda." />
<LandscapeFrameUploaded
@@ -148,15 +163,6 @@ export default function DonationEditNews() {
/>
<Spacing />
<ButtonCustom
disabled={!data?.title || !data?.deskripsi}
isLoading={isLoading}
onPress={() => {
handlerSubmitUpdate();
}}
>
Update
</ButtonCustom>
</StackCustom>
<Spacing />
</ViewWrapper>

View File

@@ -1,8 +1,10 @@
import {
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
InformationBox,
LandscapeFrameUploaded,
NewWrapper,
Spacing,
StackCustom,
TextAreaCustom,
@@ -53,7 +55,7 @@ export default function DonationAddNews() {
text1: "Gagal menambah berita",
});
return
return;
}
Toast.show({
@@ -70,7 +72,21 @@ export default function DonationAddNews() {
};
return (
<ViewWrapper>
<NewWrapper
footerComponent={
<BoxButtonOnFooter>
<ButtonCustom
disabled={!data.title || !data.deskripsi}
isLoading={isLoading}
onPress={() => {
handlerSubmit();
}}
>
Simpan
</ButtonCustom>
</BoxButtonOnFooter>
}
>
<StackCustom gap={"xs"}>
<InformationBox text="Upload gambar bersifat opsional untuk melengkapi kabar terkait donasi Anda." />
<LandscapeFrameUploaded image={image?.uri} />
@@ -116,17 +132,7 @@ export default function DonationAddNews() {
/>
<Spacing />
<ButtonCustom
disabled={!data.title || !data.deskripsi}
isLoading={isLoading}
onPress={() => {
handlerSubmit();
}}
>
Simpan
</ButtonCustom>
</StackCustom>
<Spacing />
</ViewWrapper>
</NewWrapper>
);
}

View File

@@ -1,110 +1,8 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BackButton,
BaseBox,
DrawerCustom,
Grid,
LoaderCustom,
MenuDrawerDynamicGrid,
TextCustom,
ViewWrapper,
} from "@/components";
import { IconPlus } from "@/components/_Icon";
import { apiDonationGetNewsById } from "@/service/api-client/api-donation";
import { formatChatTime } from "@/utils/formatChatTime";
import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { useLocalSearchParams } from "expo-router";
import Donation_ScreenListOfNews from "@/screens/Donation/ScreenListOfNews";
export default function DonationRecapOfNews() {
const { id } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = useState(false);
const [list, setList] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState<boolean>(false);
useFocusEffect(
useCallback(() => {
onLoadList();
}, [id])
);
const onLoadList = async () => {
try {
setLoadList(true);
const response = await apiDonationGetNewsById({
id: id as string,
category: "get-all",
});
setList(response.data);
} catch (error) {
console.log("[ERROR]", error);
setList([]);
} finally {
setLoadList(false);
}
};
return (
<>
<Stack.Screen
options={{
title: "Daftar Kabar",
headerLeft: () => <BackButton />,
}}
/>
<ViewWrapper>
{loadList ? (
<LoaderCustom />
) : _.isEmpty(list) ? (
<TextCustom align="center" color="gray">
Tidak ada kabar
</TextCustom>
) : (
list?.map((item: any, index: number) => (
<BaseBox key={index} href={`/donation/[id]/(news)/${item.id}`}>
<Grid>
<Grid.Col span={8}>
<TextCustom truncate bold>
{item?.title || "-"}
</TextCustom>
</Grid.Col>
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
<TextCustom size="small">
{formatChatTime(item?.createdAt)}
</TextCustom>
</Grid.Col>
</Grid>
</BaseBox>
))
)}
</ViewWrapper>
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
icon: <IconPlus />,
label: "Tambah Berita",
path: `/donation/${id}/(news)/add-news`,
},
]}
onPressItem={(item) => {
console.log("PATH ", item.path);
router.navigate(item.path as any);
setOpenDrawer(false);
}}
/>
</DrawerCustom>
</>
);
return <Donation_ScreenListOfNews donationId={id as string} />;
}

View File

@@ -1,112 +1,8 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BackButton,
BaseBox,
DotButton,
DrawerCustom,
Grid,
LoaderCustom,
MenuDrawerDynamicGrid,
TextCustom,
ViewWrapper,
} from "@/components";
import { IconPlus } from "@/components/_Icon";
import { apiDonationGetNewsById } from "@/service/api-client/api-donation";
import { formatChatTime } from "@/utils/formatChatTime";
import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { useLocalSearchParams } from "expo-router";
import Donation_ScreenRecapOfNews from "@/screens/Donation/ScreenRecapOfNews";
export default function DonationRecapOfNews() {
const { id } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = useState(false);
const [list, setList] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState<boolean>(false);
useFocusEffect(
useCallback(() => {
onLoadList();
}, [id])
);
const onLoadList = async () => {
try {
setLoadList(true);
const response = await apiDonationGetNewsById({
id: id as string,
category: "get-all",
});
setList(response.data);
} catch (error) {
console.log("[ERROR]", error);
setList([]);
} finally {
setLoadList(false);
}
};
return (
<>
<Stack.Screen
options={{
title: "Rekap Kabar",
headerLeft: () => <BackButton />,
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
}}
/>
<ViewWrapper>
{loadList ? (
<LoaderCustom />
) : _.isEmpty(list) ? (
<TextCustom align="center" color="gray">
Tidak ada kabar
</TextCustom>
) : (
list?.map((item: any, index: number) => (
<BaseBox key={index} href={`/donation/[id]/(news)/${item.id}`}>
<Grid>
<Grid.Col span={8}>
<TextCustom truncate bold>
{item?.title || "-"}
</TextCustom>
</Grid.Col>
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
<TextCustom size="small">
{formatChatTime(item?.createdAt)}
</TextCustom>
</Grid.Col>
</Grid>
</BaseBox>
))
)}
</ViewWrapper>
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
icon: <IconPlus />,
label: "Tambah Berita",
path: `/donation/${id}/(news)/add-news`,
},
]}
onPressItem={(item) => {
console.log("PATH ", item.path);
router.navigate(item.path as any);
setOpenDrawer(false);
}}
/>
</DrawerCustom>
</>
);
return <Donation_ScreenRecapOfNews donationId={id as string} />;
}

View File

@@ -1,6 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BaseBox,
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
Grid,
@@ -35,7 +36,7 @@ export default function DonationInvoice() {
useFocusEffect(
useCallback(() => {
onLoadData();
}, [invoiceId])
}, [invoiceId]),
);
const onLoadData = async () => {
@@ -100,7 +101,22 @@ export default function DonationInvoice() {
return (
<>
<ViewWrapper>
<ViewWrapper
hideFooter
footerComponent={
<BoxButtonOnFooter>
<ButtonCustom
disabled={!image}
isLoading={isLoading}
onPress={() => {
handlerUpdateInvoice();
}}
>
Simpan
</ButtonCustom>
</BoxButtonOnFooter>
}
>
<StackCustom>
<InformationBox
text={`Mohon transfer donasi anda ke rekening dibawah`}
@@ -204,16 +220,6 @@ export default function DonationInvoice() {
</ButtonCenteredOnly>
</StackCustom>
</BaseBox>
<ButtonCustom
disabled={!image}
isLoading={isLoading}
onPress={() => {
handlerUpdateInvoice();
}}
>
Simpan
</ButtonCustom>
</StackCustom>
<Spacing />
</ViewWrapper>

View File

@@ -4,11 +4,12 @@ import {
DotButton,
DrawerCustom,
MenuDrawerDynamicGrid,
NewWrapper,
Spacing,
ViewWrapper,
} from "@/components";
import { IconEdit, IconNews } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import Donation_ButtonStatusSection from "@/screens/Donation/ButtonStatusSection";
@@ -26,13 +27,14 @@ import {
} from "expo-router";
import _ from "lodash";
import { useCallback, useEffect, useState } from "react";
import { RefreshControl } from "react-native";
export default function DonasiDetailStatus() {
const { id, status } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = useState(false);
const [openDrawerPublish, setOpenDrawerPublish] = useState(false);
const [data, setData] = useState<any>();
const [refreshing, setRefreshing] = useState(false);
const [data, setData] = useState<any | null>(null);
useFocusEffect(
useCallback(() => {
@@ -80,6 +82,17 @@ export default function DonasiDetailStatus() {
});
};
const onRefresh = useCallback(() => {
try {
setRefreshing(true);
onLoadData();
} catch (error) {
console.log("Error refresh");
} finally {
setRefreshing(false);
}
}, []);
return (
<>
<Stack.Screen
@@ -94,32 +107,50 @@ export default function DonasiDetailStatus() {
) : null,
}}
/>
<ViewWrapper>
<Donation_ComponentBoxDetailData
sisaHari={value.sisa}
reminder={value.reminder}
data={data}
showSisaHari={status === "publish" ? true : false}
bottomSection={
status === "publish" && (
<Donation_ProgressSection
<NewWrapper
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
/>
}
>
{!data ? (
<CustomSkeleton height={400} />
) : (
<>
<Donation_ComponentBoxDetailData
sisaHari={value.sisa}
reminder={value.reminder}
data={data}
showSisaHari={status === "publish" ? true : false}
bottomSection={
status === "publish" && (
<Donation_ProgressSection
id={id as string}
progres={Number(data?.progres) || 0}
/>
)
}
/>
<Donation_ComponentStoryFunrising
id={id as string}
dataStory={data?.CeritaDonasi}
/>
<Spacing />
{data && (
<Donation_ButtonStatusSection
id={id as string}
progres={Number(data?.progres) || 0}
status={status as string}
/>
)
}
/>
<Donation_ComponentStoryFunrising
id={id as string}
dataStory={data?.CeritaDonasi}
/>
<Spacing />
<Donation_ButtonStatusSection
id={id as string}
status={status as string}
/>
<Spacing />
</ViewWrapper>
)}
<Spacing />
</>
)}
</NewWrapper>
<DrawerCustom
isVisible={openDrawer}

View File

@@ -13,6 +13,7 @@ import {
TextInputCustom,
ViewWrapper,
} from "@/components";
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
import API_IMAGE from "@/constants/api-storage";
import DIRECTORY_ID from "@/constants/directory-id";
import {
@@ -200,7 +201,7 @@ export default function DonationEdit() {
>
<InformationBox text="Lengkapi semua data di bawah untuk selanjutnya mengisi cerita penggalangan dana." />
{!data || loadList ? (
<LoaderCustom />
<ListSkeletonComponent />
) : (
<StackCustom gap={"xs"}>
<TextInputCustom

View File

@@ -1,124 +1,8 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BaseBox,
ButtonCenteredOnly,
Grid,
InformationBox,
LoaderCustom,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import {
apiDonationDisbursementOfFundsListById,
apiDonationGetOne,
} from "@/service/api-client/api-donation";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import dayjs from "dayjs";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import React, { useState } from "react";
import { useLocalSearchParams } from "expo-router";
import Donation_ScreenFundDisbursement from "@/screens/Donation/ScreenFundDisbursement";
export default function DonationFundDisbursement() {
const { id } = useLocalSearchParams();
const [data, setData] = useState({
totalPencairan: 0,
akumulasiPencairan: 0,
});
const [listData, setListData] = React.useState<any[] | null>(null);
const [loadData, setLoadData] = React.useState(false);
useFocusEffect(
React.useCallback(() => {
onLoadData();
}, [id])
);
const onLoadData = async () => {
try {
setLoadData(true);
const responseData = await apiDonationGetOne({
id: id as string,
category: "permanent",
});
if (responseData.success) {
setData({
totalPencairan: responseData.data.totalPencairan,
akumulasiPencairan: responseData.data.akumulasiPencairan,
});
}
const responseList = await apiDonationDisbursementOfFundsListById({
id: id as string,
});
if (responseList.success) {
setListData(responseList.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadData(false);
}
};
return (
<>
<ViewWrapper>
<InformationBox text="Pencairan dana akan dilakukan oleh Admin HIPMI tanpa campur tangan pihak manapun, jika berita pencairan dana dibawah tidak sesuai dengan kabar yang diberikan oleh PENGGALANG DANA. Maka pegguna lain dapat melaporkannya pada Admin HIPMI !" />
<BaseBox>
<Grid>
<Grid.Col span={6}>
<TextCustom bold color="yellow">
Rp. {formatCurrencyDisplay(data?.totalPencairan)}
</TextCustom>
<TextCustom size="small">Total Pencairan Dana</TextCustom>
</Grid.Col>
<Grid.Col span={6}>
<TextCustom bold color="yellow">
{data?.akumulasiPencairan} kali
</TextCustom>
<TextCustom size="small">Akumulasi Pencairan</TextCustom>
</Grid.Col>
</Grid>
</BaseBox>
{loadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center" color="gray">
Belum ada data
</TextCustom>
) : (
listData?.map((item, index) => (
<BaseBox key={index}>
<StackCustom>
<Grid>
<Grid.Col span={8}>
<TextCustom bold>{item?.title}</TextCustom>
</Grid.Col>
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
<TextCustom>{dayjs(item?.createdAt).format("DD MMM YYYY")}</TextCustom>
</Grid.Col>
</Grid>
<TextCustom>{item?.deskripsi}</TextCustom>
<ButtonCenteredOnly
onPress={() => {
router.navigate(`/(application)/(image)/preview-image/${item?.imageId}`);
}}
icon="file-text"
>
Bukti Transaksi
</ButtonCenteredOnly>
</StackCustom>
</BaseBox>
))
)}
</ViewWrapper>
</>
);
return <Donation_ScreenFundDisbursement donationId={id as string} />;
}

View File

@@ -6,10 +6,12 @@ import {
DotButton,
DrawerCustom,
MenuDrawerDynamicGrid,
NewWrapper,
StackCustom,
ViewWrapper,
} from "@/components";
import { IconNews } from "@/components/_Icon";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import { useAuth } from "@/hooks/use-auth";
import Donation_ComponentBoxDetailData from "@/screens/Donation/ComponentBoxDetailData";
import Donation_ComponentInfoFundrising from "@/screens/Donation/ComponentInfoFundrising";
@@ -34,7 +36,7 @@ export default function DonasiDetailBeranda() {
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
}, [id]),
);
const onLoadData = async () => {
@@ -75,10 +77,10 @@ export default function DonasiDetailBeranda() {
<>
<BoxButtonOnFooter>
<ButtonCustom
disabled={value?.reminder}
disabled={value?.reminder || !data}
onPress={() => router.navigate(`/donation/${id}/(transaction-flow)`)}
>
{value?.reminder ? "Waktu berakhir" : "Donasi"}
{!data ? "Loading..." : value?.reminder ? "Waktu berakhir" : "Donasi"}
</ButtonCustom>
</BoxButtonOnFooter>
</>
@@ -96,21 +98,30 @@ export default function DonasiDetailBeranda() {
) : null,
}}
/>
<ViewWrapper footerComponent={buttonSection}>
<StackCustom>
<Donation_ComponentBoxDetailData
sisaHari={value.sisa}
reminder={value.reminder}
data={data}
bottomSection={<Donation_ProgressSection id={id as string} progres={Number(data?.progres) || 0} />}
/>
<Donation_ComponentInfoFundrising dataAuthor={data?.Author} />
<Donation_ComponentStoryFunrising
id={id as string}
dataStory={data?.CeritaDonasi}
/>
</StackCustom>
</ViewWrapper>
<NewWrapper footerComponent={buttonSection}>
{!data ? (
<CustomSkeleton height={400} />
) : (
<StackCustom>
<Donation_ComponentBoxDetailData
sisaHari={value.sisa}
reminder={value.reminder}
data={data}
bottomSection={
<Donation_ProgressSection
id={id as string}
progres={Number(data?.progres) || 0}
/>
}
/>
<Donation_ComponentInfoFundrising dataAuthor={data?.Author} />
<Donation_ComponentStoryFunrising
id={id as string}
dataStory={data?.CeritaDonasi}
/>
</StackCustom>
)}
</NewWrapper>
<DrawerCustom
isVisible={openDrawer}

View File

@@ -1,94 +1,8 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BaseBox,
Grid,
LoaderCustom,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { apiAdminDonationListOfDonaturById } from "@/service/api-admin/api-admin-donation";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import { FontAwesome6 } from "@expo/vector-icons";
import dayjs from "dayjs";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { useLocalSearchParams } from "expo-router";
import Donation_ScreenListOfDonatur from "@/screens/Donation/ScreenListOfDonatur";
export default function Donation_ListOfDonatur() {
export default function DonationListOfDonatur() {
const { id } = useLocalSearchParams();
const [listData, setListData] = useState<any[] | null>(null);
const [loadData, setLoadData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
const onLoadData = async () => {
try {
setLoadData(true);
const response = await apiAdminDonationListOfDonaturById({
id: id as string,
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadData(false);
}
};
return (
<>
<ViewWrapper>
{loadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom bold align="center">
Belum ada donatur
</TextCustom>
) : (
listData?.map((item: any, index: number) => (
<BaseBox key={index}>
<Grid>
<Grid.Col
span={3}
style={{ alignItems: "center", justifyContent: "center" }}
>
<FontAwesome6
name="face-smile-wink"
size={50}
style={{ color: MainColor.yellow }}
/>
</Grid.Col>
<Grid.Col span={9}>
<TextCustom bold size="large">
{item?.Author?.username || "-"}
</TextCustom>
<Spacing/>
<StackCustom gap={"xs"}>
<TextCustom size={"small"}>Berdonas sebesar </TextCustom>
<TextCustom bold size="large" color="yellow">
Rp. {formatCurrencyDisplay(item?.nominal)}
</TextCustom>
<TextCustom>
{dayjs(item?.createdAt).format("DD MMM YYYY, HH:mm")}
</TextCustom>
</StackCustom>
</Grid.Col>
</Grid>
</BaseBox>
))
)}
</ViewWrapper>
</>
);
return <Donation_ScreenListOfDonatur donationId={id as string} />;
}

View File

@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react-hooks/exhaustive-deps */
import { StackCustom, ViewWrapper } from "@/components";
import { BasicWrapper, StackCustom, ViewWrapper } from "@/components";
import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import { useNotificationStore } from "@/hooks/use-notification-store";
@@ -29,14 +29,14 @@ export default function Application() {
checkVersion();
userData(token as string);
syncUnreadCount();
}, [user?.id, token])
}, [user?.id, token]),
);
async function onLoadData() {
const response = await apiUser(user?.id as string);
console.log(
"[Profile ID]>>",
JSON.stringify(response?.data?.Profile?.id, null, 2)
JSON.stringify(response?.data?.Profile?.id, null, 2),
);
setData(response.data);
@@ -61,14 +61,31 @@ export default function Application() {
if (data && data?.active === false) {
console.log("User is not active");
return <Redirect href={`/waiting-room`} />;
return (
<BasicWrapper>
<Redirect href={`/waiting-room`} />
</BasicWrapper>
);
}
if (data && data?.Profile === null) {
console.log("Profile is null");
return <Redirect href={`/profile/create`} />;
return (
<BasicWrapper>
<Redirect href={`/profile/create`} />
</BasicWrapper>
);
}
// if (data && data?.masterUserRoleId !== "1") {
// console.log("User is not admin");
// return (
// <BasicWrapper>
// <Redirect href={`/admin/dashboard`} />
// </BasicWrapper>
// );
// }
return (
<>
<Stack.Screen
@@ -89,7 +106,12 @@ export default function Application() {
/>
<ViewWrapper
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
/>
}
footerComponent={
<TabSection

View File

@@ -8,6 +8,7 @@ import {
} from "@/components";
import DrawerAdmin from "@/components/Drawer/DrawerAdmin";
import NavbarMenu from "@/components/Drawer/NavbarMenu";
import NavbarMenu_V2 from "@/components/Drawer/NavbarMenu_V2";
import { AccentColor, MainColor } from "@/constants/color-palet";
import {
ICON_SIZE_MEDIUM,
@@ -20,6 +21,10 @@ import {
adminListMenu,
superAdminListMenu,
} from "@/screens/Admin/listPageAdmin";
import {
adminListMenu_V2,
superAdminListMenu_V2,
} from "@/screens/Admin/listPageAdmin_V2";
import { GStyles } from "@/styles/global-styles";
import { FontAwesome6, Ionicons } from "@expo/vector-icons";
import { router, Stack } from "expo-router";
@@ -140,13 +145,22 @@ export default function AdminLayout() {
style={{ alignSelf: "flex-end" }}
/>
<NavbarMenu
{/* <NavbarMenu
items={
user?.masterUserRoleId === "2"
? adminListMenu
: superAdminListMenu
}
onClose={() => setOpenDrawerNavbar(false)}
/> */}
<NavbarMenu_V2
items={
user?.masterUserRoleId === "2"
? adminListMenu_V2
: superAdminListMenu_V2
}
onClose={() => setOpenDrawerNavbar(false)}
/>
</StackCustom>
</DrawerAdmin>
@@ -198,7 +212,7 @@ export default function AdminLayout() {
// size={ICON_SIZE_SMALL}
// color={MainColor.white}
// />
<AdminNotificationBell/>
<AdminNotificationBell />
),
path: "/admin/notification",
},

View File

@@ -0,0 +1,276 @@
import { AccentColor, MainColor } from "@/constants/color-palet";
import { Ionicons } from "@expo/vector-icons";
import { router, usePathname } from "expo-router";
import React, { useEffect, useRef, useState } from "react";
import {
Animated,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native";
export interface NavbarItem {
label: string;
icon?: keyof typeof Ionicons.glyphMap;
color?: string;
link?: string;
links?: {
label: string;
link: string;
}[];
initiallyOpened?: boolean;
}
interface NavbarMenuProps {
items: NavbarItem[];
onClose?: () => void;
}
export default function NavbarMenuBackup({ items, onClose }: NavbarMenuProps) {
const pathname = usePathname();
const [activeLink, setActiveLink] = useState<string | null>(null);
const [openKeys, setOpenKeys] = useState<string[]>([]); // Untuk kontrol dropdown
// Normalisasi path: hapus trailing slash
const normalizePath = (path: string) => path.replace(/\/+$/, "");
const normalizedPathname = pathname ? normalizePath(pathname) : "";
// Set activeLink saat pathname berubah
useEffect(() => {
if (normalizedPathname) {
setActiveLink(normalizedPathname);
}
}, [normalizedPathname]);
// Toggle dropdown
const toggleOpen = (label: string) => {
setOpenKeys((prev) =>
prev.includes(label) ? prev.filter((key) => key !== label) : [label]
);
};
return (
<View
style={{
// flex: 1,
// backgroundColor: MainColor.black,
marginBottom: 20,
}}
>
<ScrollView
contentContainerStyle={{
paddingVertical: 10, // Opsional: tambahkan padding
}}
// showsVerticalScrollIndicator={false} // Opsional: sembunyikan indikator scroll
>
{items.map((item) => (
<MenuItem
key={item.label}
item={item}
onClose={onClose}
activeLink={activeLink}
setActiveLink={setActiveLink}
isOpen={openKeys.includes(item.label)}
toggleOpen={() => toggleOpen(item.label)}
/>
))}
</ScrollView>
</View>
);
}
// Komponen Item Menu
function MenuItem({
item,
onClose,
activeLink,
setActiveLink,
isOpen,
toggleOpen,
}: {
item: NavbarItem;
onClose?: () => void;
activeLink: string | null;
setActiveLink: (link: string | null) => void;
isOpen: boolean;
toggleOpen: () => void;
}) {
const isActive = activeLink === item.link;
const animatedHeight = useRef(new Animated.Value(0)).current;
// Animasi saat isOpen berubah
React.useEffect(() => {
Animated.timing(animatedHeight, {
toValue: isOpen ? (item.links ? item.links.length * 40 : 0) : 0,
duration: 200,
useNativeDriver: false,
}).start();
}, [isOpen, item.links, animatedHeight]);
// Jika ada submenu
if (item.links && item.links.length > 0) {
return (
<View>
{/* Parent Item */}
<TouchableOpacity style={styles.parentItem} onPress={toggleOpen}>
<Ionicons
name={item.icon}
size={16}
color={MainColor.white}
style={styles.icon}
/>
<Text style={styles.parentText}>{item.label}</Text>
<Ionicons
name={isOpen ? "chevron-up" : "chevron-down"}
size={16}
color={MainColor.white}
/>
</TouchableOpacity>
{/* Submenu (Animated) */}
<Animated.View
style={[
styles.submenu,
// {
// backgroundColor: "red",
// },
{
height: animatedHeight,
opacity: animatedHeight.interpolate({
inputRange: [0, item.links.length * 40],
outputRange: [0, 1],
extrapolate: "clamp",
}),
},
]}
>
{item.links.map((subItem, index) => {
const isSubActive = activeLink === subItem.link;
return (
<TouchableOpacity
key={index}
style={[styles.subItem, isSubActive && styles.subItemActive]}
onPress={() => {
setActiveLink(subItem.link);
onClose?.();
router.push(subItem.link as any);
}}
>
<Ionicons
name="radio-button-on-outline"
size={16}
color={isSubActive ? MainColor.yellow : MainColor.white}
style={styles.icon}
/>
<Text
style={[
styles.subText,
isSubActive && { color: MainColor.yellow },
]}
>
{subItem.label}
</Text>
</TouchableOpacity>
);
})}
</Animated.View>
</View>
);
}
// Menu tanpa submenu
return (
<TouchableOpacity
style={[styles.singleItem, isActive && styles.singleItemActive]}
onPress={() => {
setActiveLink(item.link || null);
onClose?.();
router.push(item.link as any);
}}
>
<Ionicons
name={item.icon}
size={16}
color={isActive ? MainColor.yellow : MainColor.white}
style={styles.icon}
/>
<Text
style={[
styles.singleText,
{ color: isActive ? MainColor.yellow : MainColor.white },
]}
>
{item.label}
</Text>
</TouchableOpacity>
);
}
// Styles
const styles = StyleSheet.create({
container: {
marginBottom: 5,
},
parentItem: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 12,
paddingHorizontal: 10,
// backgroundColor: AccentColor.darkblue,
borderRadius: 8,
marginBottom: 5,
justifyContent: "space-between",
},
parentText: {
flex: 1,
fontSize: 16,
fontWeight: "500",
marginLeft: 10,
color: MainColor.white,
},
singleItem: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 12,
paddingHorizontal: 10,
// backgroundColor: AccentColor.darkblue,
borderRadius: 8,
marginBottom: 5,
},
singleItemActive: {
backgroundColor: AccentColor.blue,
},
singleText: {
fontSize: 16,
fontWeight: "500",
marginLeft: 10,
color: MainColor.white,
},
icon: {
width: 24,
textAlign: "center",
paddingRight: 10,
},
submenu: {
overflow: "hidden",
marginLeft: 30,
marginTop: 5,
},
subItem: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 8,
paddingHorizontal: 10,
borderRadius: 6,
marginBottom: 4,
},
subItemActive: {
backgroundColor: AccentColor.blue,
},
subText: {
color: MainColor.white,
fontSize: 16,
fontWeight: "500",
},
});

View File

@@ -37,6 +37,90 @@ export default function NavbarMenu({ items, onClose }: NavbarMenuProps) {
const normalizePath = (path: string) => path.replace(/\/+$/, "");
const normalizedPathname = pathname ? normalizePath(pathname) : "";
// Fungsi untuk mengecek apakah path cocok dengan item menu
// Ini akan mengecek kecocokan eksak atau pola path
const isActivePath = (itemPath: string | undefined): boolean => {
if (!itemPath || !normalizedPathname) return false;
// Cocokan eksak
if (normalizePath(itemPath) === normalizedPathname) return true;
// Cocokan pola path seperti /user-access/[id]/index dengan /user-access/index
// atau /donation/[id]/detail dengan /donation/index
const normalizedItemPath = normalizePath(itemPath);
// Jika path item adalah bagian dari path saat ini (prefix match)
if (normalizedPathname.startsWith(normalizedItemPath + '/')) return true;
// Jika path saat ini adalah bagian dari path item (misalnya /user-access/detail cocok dengan /user-access)
if (normalizedItemPath.startsWith(normalizedPathname + '/')) return true;
// Jika path item adalah bagian dari path saat ini tanpa id (misalnya /user-access/[id]/index cocok dengan /user-access/index)
const itemParts = normalizedItemPath.split('/');
const currentParts = normalizedPathname.split('/');
// Jika panjangnya sama dan hanya berbeda di bagian dinamis [id]
if (itemParts.length === currentParts.length) {
let match = true;
for (let i = 0; i < itemParts.length; i++) {
// Jika bagian path item adalah placeholder [id], abaikan
if (itemParts[i].startsWith('[') && itemParts[i].endsWith(']')) continue;
// Jika bagian path saat ini adalah ID (angka), abaikan
if (/^\d+$/.test(currentParts[i])) continue;
// Jika tidak cocok dan bukan placeholder atau ID, maka tidak cocok
if (itemParts[i] !== currentParts[i]) {
match = false;
break;
}
}
if (match) return true;
}
// Tambahkan logika khusus untuk menangani file index.tsx sebagai halaman dashboard
// Jika path saat ini adalah versi index dari path item (misalnya /admin/event/index cocok dengan /admin/event)
if (normalizedPathname === normalizedItemPath + '/index') return true;
return false;
};
// Fungsi untuk menentukan item mana yang paling spesifik aktif
// Ini akan memastikan hanya satu item yang aktif pada satu waktu
const findMostSpecificActiveItem = (): { parentLabel?: string; subItemLink?: string } | null => {
// Cek setiap item menu
for (const item of items) {
// Jika item memiliki sub-menu
if (item.links && item.links.length > 0) {
// Urutkan sub-menu berdasarkan panjang path (terpanjang dulu untuk prioritas lebih spesifik)
const sortedSubItems = [...item.links].sort((a, b) => {
if (a.link && b.link) {
return b.link.length - a.link.length; // Urutan menurun (terpanjang dulu)
}
return 0;
});
// Cek setiap sub-menu dalam urutan yang telah diurutkan
for (const subItem of sortedSubItems) {
if (isActivePath(subItem.link)) {
return { parentLabel: item.label, subItemLink: subItem.link };
}
}
}
// Jika tidak ada sub-menu yang cocok, cek item utama
if (isActivePath(item.link)) {
return { parentLabel: item.label };
}
}
return null;
};
// Hitung item aktif terlebih dahulu
const mostSpecificActive = findMostSpecificActiveItem();
// Set activeLink saat pathname berubah
useEffect(() => {
if (normalizedPathname) {
@@ -44,6 +128,15 @@ export default function NavbarMenu({ items, onClose }: NavbarMenuProps) {
}
}, [normalizedPathname]);
// Fungsi untuk menentukan apakah dropdown harus tetap terbuka
// Dropdown tetap terbuka jika salah satu dari sub-menu cocok dengan path saat ini
const shouldDropdownBeOpen = (item: NavbarItem): boolean => {
if (!normalizedPathname || !item.links || item.links.length === 0) return false;
// Cek apakah salah satu sub-menu cocok dengan path saat ini
return item.links.some(subItem => isActivePath(subItem.link));
};
// Toggle dropdown
const toggleOpen = (label: string) => {
setOpenKeys((prev) =>
@@ -56,7 +149,7 @@ export default function NavbarMenu({ items, onClose }: NavbarMenuProps) {
style={{
// flex: 1,
// backgroundColor: MainColor.black,
marginBottom: 20,
marginBottom: 20,
}}
>
<ScrollView
@@ -72,8 +165,21 @@ export default function NavbarMenu({ items, onClose }: NavbarMenuProps) {
onClose={onClose}
activeLink={activeLink}
setActiveLink={setActiveLink}
isOpen={openKeys.includes(item.label)}
isOpen={openKeys.includes(item.label) || shouldDropdownBeOpen(item)}
toggleOpen={() => toggleOpen(item.label)}
isActivePath={isActivePath}
isMostSpecificActive={(menuItem) => {
if (!mostSpecificActive) return false;
// Jika item memiliki sub-menu
if (menuItem.links && menuItem.links.length > 0) {
// Jika item ini adalah parent dari sub-menu yang aktif, menu utama tidak aktif
return false;
}
// Jika tidak ada sub-menu, hanya periksa kecocokan langsung
return mostSpecificActive.parentLabel === menuItem.label && !mostSpecificActive.subItemLink;
}}
/>
))}
</ScrollView>
@@ -89,6 +195,8 @@ function MenuItem({
setActiveLink,
isOpen,
toggleOpen,
isActivePath,
isMostSpecificActive,
}: {
item: NavbarItem;
onClose?: () => void;
@@ -96,8 +204,10 @@ function MenuItem({
setActiveLink: (link: string | null) => void;
isOpen: boolean;
toggleOpen: () => void;
isActivePath: (itemPath: string | undefined) => boolean;
isMostSpecificActive: (item: NavbarItem) => boolean;
}) {
const isActive = activeLink === item.link;
const isActive = isMostSpecificActive(item);
const animatedHeight = useRef(new Animated.Value(0)).current;
// Animasi saat isOpen berubah
@@ -121,7 +231,9 @@ function MenuItem({
color={MainColor.white}
style={styles.icon}
/>
<Text style={styles.parentText}>{item.label}</Text>
<Text style={styles.parentText}>
{item.label}
</Text>
<Ionicons
name={isOpen ? "chevron-up" : "chevron-down"}
size={16}
@@ -147,7 +259,8 @@ function MenuItem({
]}
>
{item.links.map((subItem, index) => {
const isSubActive = activeLink === subItem.link;
// Untuk sub-item, kita gunakan logika aktif berdasarkan isActivePath
const isSubActive = isActivePath(subItem.link);
return (
<TouchableOpacity
key={index}

View File

@@ -0,0 +1,570 @@
import { AccentColor, MainColor } from "@/constants/color-palet";
import { Ionicons } from "@expo/vector-icons";
import { router, usePathname } from "expo-router";
import { useEffect, useRef, useState } from "react";
import {
Animated,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native";
export interface NavbarItem_V2 {
label: string;
icon?: keyof typeof Ionicons.glyphMap;
color?: string;
link?: string;
links?: {
label: string;
link: string;
detailPattern?: string;
}[];
initiallyOpened?: boolean;
}
interface NavbarMenuProps {
items: NavbarItem_V2[];
onClose?: () => void;
}
export default function NavbarMenu_V2({ items, onClose }: NavbarMenuProps) {
const pathname = usePathname();
const [openKeys, setOpenKeys] = useState<string[]>([]);
// Normalisasi path: hapus trailing slash
const normalizePath = (path: string) => path.replace(/\/+$/, "");
const normalizedPathname = pathname ? normalizePath(pathname) : "";
// Auto-open parent menu jika submenu aktif
useEffect(() => {
if (!normalizedPathname || !items || items.length === 0) {
return;
}
try {
const newOpenKeys: string[] = [];
// Helper function yang sama dengan di MenuItem
const checkPathMatch = (linkPath: string, detailPattern?: string) => {
const normalizedLink = linkPath.replace(/\/+$/, "");
// Exact match
if (normalizedPathname === normalizedLink) return true;
// Detail pattern match
if (detailPattern) {
const patternRegex = new RegExp(
"^" + detailPattern.replace(/\*/g, "[^/]+") + "(/.*)?$",
);
if (patternRegex.test(normalizedPathname)) {
return true;
}
}
// Detail page match (fallback)
if (normalizedPathname.startsWith(normalizedLink + "/")) {
const remainder = normalizedPathname.substring(
normalizedLink.length + 1,
);
const segments = remainder.split("/").filter((s) => s.length > 0);
if (segments.length === 0) return false;
const commonWords = [
// Event
"type-create",
// Other
"detail",
"edit",
"create",
"new",
"add",
"delete",
"view",
"publish",
"review",
"reject",
"status",
"category",
"history",
"type-of-event",
"posting",
"report-posting",
"report-comment",
"group",
"dashboard",
"sticker",
"active",
"inactive",
"pending",
"transaction-detail",
"transaction",
"payment",
"disbursement",
"list-of-investor",
];
const hasIdSegment = segments.some((segment) => {
if (commonWords.includes(segment.toLowerCase())) {
return false;
}
const isPureNumber = /^\d+$/.test(segment);
const isUUID =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
segment,
);
const hasNumber = /\d/.test(segment);
const isAlphanumericId =
/^[a-z0-9_-]+$/i.test(segment) &&
segment.length <= 50 &&
hasNumber;
return isPureNumber || isUUID || isAlphanumericId;
});
return hasIdSegment;
}
return false;
};
items.forEach((item) => {
if (item.links && item.links.length > 0) {
// Check jika ada submenu yang match dengan current path
const hasActiveSubmenu = item.links.some((subItem) => {
return checkPathMatch(subItem.link, subItem.detailPattern);
});
if (hasActiveSubmenu) {
newOpenKeys.push(item.label);
}
}
});
setOpenKeys(newOpenKeys);
} catch (error) {
console.error("Error in NavbarMenu useEffect:", error);
}
}, [normalizedPathname, items]);
// Toggle dropdown
const toggleOpen = (label: string) => {
setOpenKeys((prev) =>
prev.includes(label)
? prev.filter((key) => key !== label)
: [...prev, label],
);
};
return (
<View
style={{
marginBottom: 20,
}}
>
<ScrollView
contentContainerStyle={{
paddingVertical: 10,
}}
>
{items && items.length > 0
? items.map((item) => (
<MenuItem
key={item.label}
item={item}
onClose={onClose}
currentPath={normalizedPathname}
isOpen={openKeys.includes(item.label)}
toggleOpen={() => toggleOpen(item.label)}
/>
))
: null}
</ScrollView>
</View>
);
}
// Komponen Item Menu
function MenuItem({
item,
onClose,
currentPath,
isOpen,
toggleOpen,
}: {
item: NavbarItem_V2;
onClose?: () => void;
currentPath: string;
isOpen: boolean;
toggleOpen: () => void;
}) {
const animatedHeight = useRef(new Animated.Value(0)).current;
// Helper function untuk check apakah path aktif
const isPathActive = (
linkPath: string | undefined,
detailPattern?: string,
) => {
if (!linkPath) return false;
const normalizedLink = linkPath.replace(/\/+$/, "");
// 1. Match exact - prioritas tertinggi
if (currentPath === normalizedLink) return true;
// 2. Jika ada detailPattern, cek pattern dulu
if (detailPattern) {
// detailPattern contoh: "/admin/job/*/review"
// akan match dengan:
// - /admin/job/123/review ✅
// - /admin/job/123/review/transaction-detail ✅
// - /admin/job/123/review/anything/nested ✅
const patternRegex = new RegExp(
"^" + detailPattern.replace(/\*/g, "[^/]+") + "(/.*)?$",
);
const isMatch = patternRegex.test(currentPath);
// Debug log untuk pattern matching
if (
currentPath.includes("list-of-investor") ||
currentPath.includes("type-create")
) {
console.log(
"🔍 Pattern Match Check:",
JSON.stringify(
{
currentPath,
detailPattern,
regex: patternRegex.toString(),
isMatch,
},
null,
2,
),
);
}
if (isMatch) {
return true;
}
}
// 3. Match untuk detail pages (fallback)
if (currentPath.startsWith(normalizedLink + "/")) {
const remainder = currentPath.substring(normalizedLink.length + 1);
const segments = remainder.split("/").filter((s) => s.length > 0);
if (segments.length === 0) return false;
const commonWords = [
// Event
"type-create",
"detail",
"edit",
"create",
"new",
"add",
"delete",
"view",
"publish",
"review",
"reject",
"status",
"category",
"history",
"type-of-event",
"posting",
"report-posting",
"report-comment",
"group",
"dashboard",
"sticker",
"active",
"inactive",
"pending",
"transaction-detail",
"transaction",
"payment",
"disbursement",
];
const hasIdSegment = segments.some((segment) => {
if (commonWords.includes(segment.toLowerCase())) {
return false;
}
const isPureNumber = /^\d+$/.test(segment);
const isUUID =
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(
segment,
);
const hasNumber = /\d/.test(segment);
const isAlphanumericId =
/^[a-z0-9_-]+$/i.test(segment) && segment.length <= 50 && hasNumber;
return isPureNumber || isUUID || isAlphanumericId;
});
return hasIdSegment;
}
return false;
};
// Check apakah menu item ini atau submenu-nya yang aktif
const isActive = isPathActive(item.link);
const hasActiveSubmenu =
item.links?.some((subItem) =>
isPathActive(subItem.link, subItem.detailPattern),
) || false;
// Animasi saat isOpen berubah
useEffect(() => {
Animated.timing(animatedHeight, {
toValue: isOpen ? (item.links ? item.links.length * 44 : 0) : 0,
duration: 200,
useNativeDriver: false,
}).start();
}, [isOpen, item.links, animatedHeight]);
// Jika ada submenu
if (item.links && item.links.length > 0) {
return (
<View>
{/* Parent Item */}
<TouchableOpacity
style={[
styles.parentItem,
hasActiveSubmenu && styles.parentItemActive,
]}
onPress={toggleOpen}
>
<Ionicons
name={item.icon}
size={16}
color={hasActiveSubmenu ? MainColor.yellow : MainColor.white}
style={styles.icon}
/>
<Text
style={[
styles.parentText,
hasActiveSubmenu && { color: MainColor.yellow },
]}
>
{item.label}
</Text>
<Ionicons
name={isOpen ? "chevron-up" : "chevron-down"}
size={16}
color={hasActiveSubmenu ? MainColor.yellow : MainColor.white}
/>
</TouchableOpacity>
{/* Submenu (Animated) */}
<Animated.View
style={[
styles.submenu,
{
height: animatedHeight,
opacity: animatedHeight.interpolate({
inputRange: [0, item.links.length * 44],
outputRange: [0, 1],
extrapolate: "clamp",
}),
},
]}
>
{item.links.map((subItem, index) => {
const isSubActive = isPathActive(
subItem.link,
subItem.detailPattern,
);
// CRITICAL FIX: Jika submenu ini aktif, cek apakah ada submenu lain yang LEBIH PANJANG dan juga aktif
// Jika ada yang lebih panjang dan aktif, maka yang pendek TIDAK AKTIF
const hasMoreSpecificMatch = item.links!.some((otherSubItem) => {
if (otherSubItem.link === subItem.link) return false; // Skip self
const otherIsActive = isPathActive(
otherSubItem.link,
otherSubItem.detailPattern,
);
const isOtherLonger =
otherSubItem.link.length > subItem.link.length;
// Debug log
if (isSubActive && otherIsActive) {
console.log(
"🔍 CONFLICT DETECTED:",
JSON.stringify(
{
current: subItem.label,
currentPath: subItem.link,
currentLength: subItem.link.length,
other: otherSubItem.label,
otherPath: otherSubItem.link,
otherLength: otherSubItem.link.length,
isOtherLonger,
currentURL: currentPath,
},
null,
2,
),
);
}
// Jika submenu lain JUGA aktif DAN lebih panjang (lebih spesifik),
// maka submenu yang pendek ini TIDAK boleh aktif
return otherIsActive && isOtherLonger;
});
// Final decision: aktif HANYA jika match DAN tidak ada yang lebih spesifik
const finalIsActive = isSubActive && !hasMoreSpecificMatch;
// Debug final decision
if (isSubActive) {
console.log(
"✅ Active check:",
JSON.stringify(
{
label: subItem.label,
link: subItem.link,
isSubActive,
hasMoreSpecificMatch,
finalIsActive,
},
null,
2,
),
);
}
return (
<TouchableOpacity
key={index}
style={[styles.subItem, finalIsActive && styles.subItemActive]}
onPress={() => {
onClose?.();
router.push(subItem.link as any);
}}
>
<Ionicons
name="radio-button-on-outline"
size={16}
color={finalIsActive ? MainColor.yellow : MainColor.white}
style={styles.icon}
/>
<Text
style={[
styles.subText,
finalIsActive && { color: MainColor.yellow },
]}
>
{subItem.label}
</Text>
</TouchableOpacity>
);
})}
</Animated.View>
</View>
);
}
// Menu tanpa submenu
return (
<TouchableOpacity
style={[styles.singleItem, isActive && styles.singleItemActive]}
onPress={() => {
onClose?.();
router.push(item.link as any);
}}
>
<Ionicons
name={item.icon}
size={16}
color={isActive ? MainColor.yellow : MainColor.white}
style={styles.icon}
/>
<Text
style={[
styles.singleText,
{ color: isActive ? MainColor.yellow : MainColor.white },
]}
>
{item.label}
</Text>
</TouchableOpacity>
);
}
// Styles
const styles = StyleSheet.create({
container: {
marginBottom: 5,
},
parentItem: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 12,
paddingHorizontal: 10,
borderRadius: 8,
marginBottom: 5,
justifyContent: "space-between",
},
parentItemActive: {
backgroundColor: AccentColor.blue,
},
parentText: {
flex: 1,
fontSize: 16,
fontWeight: "500",
marginLeft: 10,
color: MainColor.white,
},
singleItem: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 12,
paddingHorizontal: 10,
borderRadius: 8,
marginBottom: 5,
},
singleItemActive: {
backgroundColor: AccentColor.blue,
},
singleText: {
fontSize: 16,
fontWeight: "500",
marginLeft: 10,
color: MainColor.white,
},
icon: {
width: 24,
textAlign: "center",
paddingRight: 10,
},
submenu: {
overflow: "hidden",
marginLeft: 30,
marginTop: 5,
},
subItem: {
flexDirection: "row",
alignItems: "center",
paddingVertical: 8,
paddingHorizontal: 10,
borderRadius: 6,
marginBottom: 4,
},
subItemActive: {
backgroundColor: AccentColor.blue,
},
subText: {
color: MainColor.white,
fontSize: 16,
fontWeight: "500",
},
});

View File

@@ -0,0 +1,16 @@
import { MainColor } from "@/constants/color-palet";
import { View } from "react-native";
export default function BasicWrapper({
children,
}: {
children: React.ReactNode;
}) {
return (
<>
<View style={{ flex: 1, backgroundColor: MainColor.darkblue }}>
{children}
</View>
</>
);
}

View File

@@ -60,6 +60,7 @@ import SearchInput from "./_ShareComponent/SearchInput";
import DummyLandscapeImage from "./_ShareComponent/DummyLandscapeImage";
import GridComponentView from "./_ShareComponent/GridSectionView";
import NewWrapper from "./_ShareComponent/NewWrapper";
import BasicWrapper from "./_ShareComponent/BasicWrapper";
// Progress
import ProgressCustom from "./Progress/ProgressCustom";
// Loader
@@ -121,6 +122,7 @@ export {
GridComponentView,
Spacing,
NewWrapper,
BasicWrapper,
// Stack
StackCustom,
TabBarBackground,

View File

@@ -1,13 +1,13 @@
<!-- ===================== Start Penerapan Pagination ===================== -->
<!-- ===================== Start Penerapan Pagination Dari Source ===================== -->
File source: app/(application)/(user)/donation/(tabs)/status.tsx
File source: app/(application)/(user)/donation/[id]/fund-disbursement.tsx
Folder tujuan: screens/Donation
Nama file utama: ScreenStatus.tsx
Nama file utama: ScreenFundDisbursement.tsx
Buat file baru pada "Folder tujuan" dengan nama "Nama file utama" dan ubah nama function menjadi "Donation_ScreenStatus" kemudian clean code, import dan panggil function tersebut pada file "File source"
Buat file baru pada "Folder tujuan" dengan nama "Nama file utama" dan ubah nama function menjadi "Donation_ScreenFundDisbursement" kemudian clean code, import dan panggil function tersebut pada file "File source"
Selanjutnya terapkan pagination pada file "Nama file utama"
Function fecth: apiDonationGetByStatus
Function fecth: apiDonationDisbursementOfFundsListById
File function fetch: service/api-client/api-donation.ts
File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
@@ -22,15 +22,15 @@ Jika tidak ada props page maka tambahkan props page dan default page: "1"
Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
<!-- Additional Prompt -->
File refrensi: screens/Event/ScreenStatus.tsx
File refrensi: screens/Donation/ScreenListOfNews.tsx
Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan tipe fitur yang sama
<!-- ===================== End Penerapan Pagination ===================== -->
<!-- ===================== End Penerapan Pagination ` ===================== -->
<!-- ===================== Start Penerapan NewWrapper ===================== -->
File utama: screens/Invesment/ScreenTransaction.tsx
Function fecth: apiInvestmentGetInvoice
File function fetch: service/api-client/api-investment.ts
<!-- ===================== Start Penerapan NewWrapper & Pagination ===================== -->
File utama: screens/Donation/ScreenFundDisbursement.tsx
Function fecth: apiDonationDisbursementOfFundsListById
File function fetch: service/api-client/api-donation.ts
File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
Terapkan pagination pada file "File utama"
@@ -43,7 +43,28 @@ Jika tidak ada props page maka tambahkan props page dan default page: "1"
Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
<!-- ===================== End Penerapan NewWrapper ===================== -->
<!-- Additinal prompt -->
Masukan kode berikut di prop ListHeaderComponent:
<InformationBox text="Pencairan dana akan dilakukan oleh Admin HIPMI tanpa campur tangan pihak manapun, jika berita pencairan dana dibawah tidak sesuai dengan kabar yang diberikan oleh PENGGALANG DANA. Maka pegguna lain dapat melaporkannya pada Admin HIPMI !" />
<BaseBox>
<Grid>
<Grid.Col span={6}>
<TextCustom bold color="yellow">
Rp. {formatCurrencyDisplay(data?.totalPencairan)}
</TextCustom>
<TextCustom size="small">Total Pencairan Dana</TextCustom>
</Grid.Col>
<Grid.Col span={6}>
<TextCustom bold color="yellow">
{data?.akumulasiPencairan} kali
</TextCustom>
<TextCustom size="small">Akumulasi Pencairan</TextCustom>
</Grid.Col>
</Grid>
</BaseBox>
<!-- ===================== End Penerapan NewWrapper & Pagination ===================== -->
<!-- Start Penerapan NewWrapper -->
Terapkan NewWrapper pada file: app/(application)/(user)/donation/create.tsx
@@ -57,3 +78,44 @@ jika tidak maka terapkan sesuai dengan logika yang sudah ada
Bagaimana menangani bug berikut pada file berikut: screens/Invesment/Document/ScreenRecap.tsx
Ini adalah halaman yang memiliki fungsi pagination , saya membuat data dummy dimana menghasilkan data urut 1-9, saya mencoba memuat halaman setiap page nya 4 saja untuk percobaan.
Saat awal muncul komponent box dengan data 9 - 6, kemudian saya hapus data ke 8 . lalu saya coba scroll ke bawah seharusnya angka akan tetap urut 9, 7, 6, 5, 4 ... 1. Tapi dalam case ini setelah 8 di hapus kemudian saya scroll box ke 5 tidak muncul saat di scroll. Apakah anda mengerti maksud saya ?
<!-- COMMIT & PUSH-->
Branch: loaddata/10-feb-26
Jalankan perintah ini: git checkout -b "Branch"
Setelah itu jalankan perintah ini: git add .
Setelah itu jalankan perintah ini: git commit -m "
<Berikan semua catatan perubahan pada branch ini, tampilan pada saya dan pastikan dalam bahasa indonesia. Saya akan cek baru saya akan berikan perintah push>
"
Setelah itu jalankan perintah ini: git push origin "Branch"
<!-- Start Random Prompt -->
Saya memiliki case pada file ini: @components/Drawer/NavbarMenu.tsx
Pada file ini saya ingin jika saat pindah halaman ( ke detail contoh : /user-access/[id]/index.tsx) maka navbar tetap menandai menu yang sedang aktif, tapi yang terjadi sekarang jika masuk ke detail maka warnanya hilang karena tidak mendeteksi halaman tersebut.
Apakah anda paham maksud saya ?
Ya, dalam fitur yang anda perbaharui masih terjadi bug. Saya akan berikan case nya secara perlahan
Saat klik sebuah menu maka sub menu akan terbuka
Saat klik sub menu maka sub menu maka akan menuju ke halaman sesuai path
Dalam bug diawal tadi untuk menu yang aktif jika masuk ke detail memang terselesaikan. Tapi muncul bug baru jika menu tersebut memiliki sub menu dan jika sub menu tersebut di klik (kecuali dashboard) yang aktif adalah bagian sub menu dashbaord dan sub menu yang kita klik, tapi jika sub menu yang di klik adalah dashboard maka semau sub menu aktif. Apakah anda mengerti maksud dari pernyataan saya ? Jika masih kurang paham saya bisa berikan masukan yang lain
Masih terjadi bug, mengapa saat klik menu yang memiliki dashboard maka sub menu dashboard dan sub menu yang kita klik menjadi aktif ?
<!-- End Random Prompt -->
export interface NavbarItem_V2 {
label: string;
icon?: keyof typeof Ionicons.glyphMap;
color?: string;
link?: string;
links?: {
label: string;
link: string;
detailPattern?: string;
}[];
initiallyOpened?: boolean;
}

View File

@@ -0,0 +1,461 @@
import { NavbarItem_V2 } from "@/components/Drawer/NavbarMenu_V2";
export { adminListMenu_V2, superAdminListMenu_V2 }
const adminListMenu_V2: NavbarItem_V2[] = [
{
label: "Main Dashboard",
icon: "home",
link: "/admin/dashboard",
},
{
label: "Investasi",
icon: "wallet",
links: [
{
label: "Dashboard",
link: "/admin/investment",
// Dashboard tidak perlu detailPattern, akan auto-match dengan /admin/investment/123/...
},
{
label: "Publish",
link: "/admin/investment/publish/status",
detailPattern: "/admin/investment/*/publish", // Match: /admin/investment/123/publish
},
{
label: "Review",
link: "/admin/investment/review/status",
detailPattern: "/admin/investment/*/review", // Match: /admin/investment/123/review
},
{
label: "Reject",
link: "/admin/investment/reject/status",
detailPattern: "/admin/investment/*/reject", // Match: /admin/investment/123/reject
},
],
},
{
label: "Donasi",
icon: "hand-right",
links: [
{
label: "Dashboard",
link: "/admin/donation",
},
{
label: "Publish",
link: "/admin/donation/publish/status",
detailPattern: "/admin/donation/*/publish",
},
{
label: "Review",
link: "/admin/donation/review/status",
detailPattern: "/admin/donation/*/review",
},
{
label: "Reject",
link: "/admin/donation/reject/status",
detailPattern: "/admin/donation/*/reject",
},
{
label: "Kategori",
link: "/admin/donation/category",
},
],
},
{
label: "Event",
icon: "calendar-clear",
links: [
{
label: "Dashboard",
link: "/admin/event",
},
{
label: "Publish",
link: "/admin/event/publish/status",
detailPattern: "/admin/event/*/publish",
},
{
label: "Review",
link: "/admin/event/review/status",
detailPattern: "/admin/event/*/review",
},
{
label: "Reject",
link: "/admin/event/reject/status",
detailPattern: "/admin/event/*/reject",
},
{
label: "Tipe Acara",
link: "/admin/event/type-of-event",
},
{
label: "Riwayat",
link: "/admin/event/history/status",
detailPattern: "/admin/event/*/history",
},
],
},
{
label: "Voting",
icon: "accessibility-outline",
links: [
{
label: "Dashboard",
link: "/admin/voting",
},
{
label: "Publish",
link: "/admin/voting/publish/status",
detailPattern: "/admin/voting/*/publish",
},
{
label: "Review",
link: "/admin/voting/review/status",
detailPattern: "/admin/voting/*/review",
},
{
label: "Reject",
link: "/admin/voting/reject/status",
detailPattern: "/admin/voting/*/reject",
},
{
label: "Riwayat",
link: "/admin/voting/history",
detailPattern: "/admin/voting/*/history",
},
],
},
{
label: "Job",
icon: "desktop-outline",
links: [
{
label: "Dashboard",
link: "/admin/job",
},
{
label: "Publish",
link: "/admin/job/publish/status",
detailPattern: "/admin/job/*/publish",
},
{
label: "Review",
link: "/admin/job/review/status",
detailPattern: "/admin/job/*/review",
},
{
label: "Reject",
link: "/admin/job/reject/status",
detailPattern: "/admin/job/*/reject",
},
],
},
{
label: "Forum",
icon: "chatbubble-ellipses-outline",
links: [
{
label: "Dashboard",
link: "/admin/forum",
},
{
label: "Posting",
link: "/admin/forum/posting",
},
{
label: "Report Posting",
link: "/admin/forum/report-posting",
},
{
label: "Report Komentar",
link: "/admin/forum/report-comment",
},
],
},
{
label: "Collaboration",
icon: "people",
links: [
{
label: "Dashboard",
link: "/admin/collaboration",
},
{
label: "Publish",
link: "/admin/collaboration/publish",
},
{
label: "Group",
link: "/admin/collaboration/group",
},
{
label: "Reject",
link: "/admin/collaboration/reject",
},
],
},
{
label: "Maps",
icon: "map",
link: "/admin/maps",
},
{
label: "App Information",
icon: "information-circle",
link: "/admin/app-information",
},
{
label: "User Access",
icon: "people",
link: "/admin/user-access",
},
];
const superAdminListMenu_V2: NavbarItem_V2[] = [
{
label: "Main Dashboard",
icon: "home",
link: "/admin/dashboard",
},
{
label: "Investasi",
icon: "wallet",
links: [
{
label: "Dashboard",
link: "/admin/investment",
},
{
label: "Publish",
link: "/admin/investment/publish/status",
detailPattern: "/admin/investment/*/publish",
},
{
label: "Review",
link: "/admin/investment/review/status",
detailPattern: "/admin/investment/*/review",
},
{
label: "Reject",
link: "/admin/investment/reject/status",
detailPattern: "/admin/investment/*/reject",
},
],
},
{
label: "Donasi",
icon: "hand-right",
links: [
{
label: "Dashboard",
link: "/admin/donation",
},
{
label: "Publish",
link: "/admin/donation/publish/status",
detailPattern: "/admin/donation/*/publish",
},
{
label: "Review",
link: "/admin/donation/review/status",
detailPattern: "/admin/donation/*/review",
},
{
label: "Reject",
link: "/admin/donation/reject/status",
detailPattern: "/admin/donation/*/reject",
},
{
label: "Kategori",
link: "/admin/donation/category",
},
],
},
{
label: "Event",
icon: "calendar-clear",
links: [
{
label: "Dashboard",
link: "/admin/event",
},
{
label: "Publish",
link: "/admin/event/publish/status",
detailPattern: "/admin/event/*/publish",
},
{
label: "Review",
link: "/admin/event/review/status",
detailPattern: "/admin/event/*/review",
},
{
label: "Reject",
link: "/admin/event/reject/status",
detailPattern: "/admin/event/*/reject",
},
{
label: "Tipe Acara",
link: "/admin/event/type-of-event",
},
{
label: "Riwayat",
link: "/admin/event/history/status",
detailPattern: "/admin/event/*/history",
},
],
},
{
label: "Voting",
icon: "accessibility-outline",
links: [
{
label: "Dashboard",
link: "/admin/voting",
},
{
label: "Publish",
link: "/admin/voting/publish/status",
detailPattern: "/admin/voting/*/publish",
},
{
label: "Review",
link: "/admin/voting/review/status",
detailPattern: "/admin/voting/*/review",
},
{
label: "Reject",
link: "/admin/voting/reject/status",
detailPattern: "/admin/voting/*/reject",
},
{
label: "Riwayat",
link: "/admin/voting/history",
detailPattern: "/admin/voting/*/history",
},
],
},
{
label: "Job",
icon: "desktop-outline",
links: [
{
label: "Dashboard",
link: "/admin/job",
},
{
label: "Publish",
link: "/admin/job/publish/status",
detailPattern: "/admin/job/*/publish",
},
{
label: "Review",
link: "/admin/job/review/status",
detailPattern: "/admin/job/*/review",
},
{
label: "Reject",
link: "/admin/job/reject/status",
detailPattern: "/admin/job/*/reject",
},
],
},
{
label: "Forum",
icon: "chatbubble-ellipses-outline",
links: [
{
label: "Dashboard",
link: "/admin/forum",
},
{
label: "Posting",
link: "/admin/forum/posting",
},
{
label: "Report Posting",
link: "/admin/forum/report-posting",
},
{
label: "Report Komentar",
link: "/admin/forum/report-comment",
},
],
},
{
label: "Collaboration",
icon: "people",
links: [
{
label: "Dashboard",
link: "/admin/collaboration",
},
{
label: "Publish",
link: "/admin/collaboration/publish",
},
{
label: "Group",
link: "/admin/collaboration/group",
},
{
label: "Reject",
link: "/admin/collaboration/reject",
},
],
},
{
label: "Maps",
icon: "map",
link: "/admin/maps",
},
{
label: "App Information",
icon: "information-circle",
link: "/admin/app-information",
},
{
label: "User Access",
icon: "people",
link: "/admin/user-access",
},
{
label: "Super Admin",
icon: "globe",
link: "/admin/super-admin",
},
];
/*
=================================================================================
PENJELASAN detailPattern:
=================================================================================
detailPattern digunakan untuk match dengan URL detail page yang strukturnya:
/admin/{module}/[id]/[status]
Contoh untuk Job Review:
- Link: /admin/job/review/status (halaman list review)
- detailPattern: /admin/job/* /review (detail dari review)
- Match dengan: /admin/job/123/review, /admin/job/456/review, dll
Wildcard "*" akan match dengan ID apapun (angka, UUID, alphanumeric).
Modul yang PERLU detailPattern:
✅ Investasi - Publish, Review, Reject (ada [id]/[status])
✅ Donasi - Publish, Review, Reject (ada [id]/[status])
✅ Event - Publish, Review, Reject, Riwayat (ada [id]/[status])
✅ Voting - Publish, Review, Reject, Riwayat (ada [id]/[status])
✅ Job - Publish, Review, Reject (ada [id]/[status])
Modul yang TIDAK PERLU detailPattern:
❌ Forum - posting, report-posting, report-comment (struktur berbeda)
❌ Collaboration - struktur berbeda
❌ Maps, App Information, User Access - single page
❌ Dashboard submenu - auto-match dengan parent path
=================================================================================
*/

View File

@@ -0,0 +1,21 @@
import { BaseBox, Grid, Spacing, TextCustom } from "@/components";
import { formatChatTime } from "@/utils/formatChatTime";
export default function Donation_BoxNews({item}: {item: any}){
return <>
<BaseBox href={`/donation/[id]/(news)/${item?.id}`}>
<Grid>
<Grid.Col span={8}>
<TextCustom truncate bold>
{item?.title || "-"}
</TextCustom>
</Grid.Col>
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
<TextCustom size="small">
{formatChatTime(item?.createdAt)}
</TextCustom>
</Grid.Col>
</Grid>
</BaseBox>
</>
}

View File

@@ -0,0 +1,62 @@
import FloatingButton from "@/components/Button/FloatingButton";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import Donation_BoxPublish from "@/screens/Donation/BoxPublish";
import { apiDonationGetAll } from "@/service/api-client/api-donation";
import { router } from "expo-router";
import { RefreshControl } from "react-native";
export default function Donation_ScreenBeranda() {
const pagination = usePagination({
fetchFunction: async (page, search) => {
return await apiDonationGetAll({
category: "beranda",
page: String(page),
}).then((res) => {
console.log("RES", JSON.stringify(res, null, 2));
return res;
});
},
pageSize: PAGINATION_DEFAULT_TAKE, // Sesuaikan dengan jumlah item per halaman yang diinginkan
onError: (error) => console.error("[ERROR] Fetch event beranda:", error),
dependencies: [],
});
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
isInitialLoad: pagination.isInitialLoad,
emptyMessage: "Belum ada donasi",
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 150,
});
return (
<NewWrapper
listData={pagination.listData}
renderItem={({ item }) => (
<Donation_BoxPublish data={item} id={item.id} />
)}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
/>
}
hideFooter
floatingButton={
<FloatingButton onPress={() => router.push("/donation/create")} />
}
/>
);
}

View File

@@ -0,0 +1,174 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ActionIcon,
BaseBox,
Grid,
InformationBox,
Spacing,
StackCustom,
TextCustom,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { usePagination } from "@/hooks/use-pagination";
import {
apiDonationDisbursementOfFundsListById,
apiDonationGetOne,
} from "@/service/api-client/api-donation";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import { Feather } from "@expo/vector-icons";
import dayjs from "dayjs";
import { router, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import React, { useState } from "react";
import { RefreshControl, View } from "react-native";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { Divider } from "react-native-paper";
interface Donation_ScreenFundDisbursementProps {
donationId: string;
}
export default function Donation_ScreenFundDisbursement({
donationId,
}: Donation_ScreenFundDisbursementProps) {
const [data, setData] = useState({
totalPencairan: 0,
akumulasiPencairan: 0,
});
// Ambil data utama (total pencairan, dll) terpisah dari pagination
React.useEffect(() => {
const onLoadData = async () => {
try {
const responseData = await apiDonationGetOne({
id: donationId,
category: "permanent",
});
if (responseData.success) {
setData({
totalPencairan: responseData.data.totalPencairan,
akumulasiPencairan: responseData.data.akumulasiPencairan,
});
}
} catch (error) {
console.log("[ERROR]", error);
}
};
onLoadData();
}, [donationId]);
const pagination = usePagination({
fetchFunction: async (page) => {
return await apiDonationDisbursementOfFundsListById({
id: donationId,
page: String(page),
});
},
pageSize: PAGINATION_DEFAULT_TAKE, // Sesuaikan dengan jumlah item per halaman dari API
dependencies: [donationId],
});
const renderItem = ({ item, index }: { item: any; index: number }) => (
<BaseBox key={index}>
<StackCustom>
<Grid>
<Grid.Col span={8}>
<TextCustom bold>{item?.title}</TextCustom>
</Grid.Col>
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
<TextCustom size="small">
{dayjs(item?.createdAt).format("DD MMM YYYY")}
</TextCustom>
</Grid.Col>
</Grid>
<TextCustom>{item?.deskripsi}</TextCustom>
{/* <Spacing /> */}
<Divider />
<Grid>
<Grid.Col span={8} style={{ alignSelf: "center" }}>
<TextCustom bold size={"large"}>
Rp. {formatCurrencyDisplay(item?.nominalCair)}
</TextCustom>
</Grid.Col>
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
<ActionIcon
icon={<Feather name="file-text" color={MainColor.black} />}
onPress={() => {
router.navigate(
`/(application)/(image)/preview-image/${item?.imageId}`,
);
}}
/>
</Grid.Col>
</Grid>
{/*
<ButtonCenteredOnly
onPress={() => {
router.navigate(
`/(application)/(image)/preview-image/${item?.imageId}`,
);
}}
icon="file-text"
>
Bukti Transaksi
</ButtonCenteredOnly> */}
</StackCustom>
</BaseBox>
);
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
isInitialLoad: pagination.isInitialLoad,
emptyMessage: "Belum ada data",
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 150,
});
// Komponen header yang akan ditampilkan di atas daftar
const ListHeaderComponent = (
<View>
<InformationBox text="Pencairan dana akan dilakukan oleh Admin HIPMI tanpa campur tangan pihak manapun, jika berita pencairan dana dibawah tidak sesuai dengan kabar yang diberikan oleh PENGGALANG DANA. Maka pegguna lain dapat melaporkannya pada Admin HIPMI !" />
<BaseBox>
<Grid>
<Grid.Col span={6}>
<TextCustom bold color="yellow">
Rp. {formatCurrencyDisplay(data?.totalPencairan)}
</TextCustom>
<TextCustom size="small">Total Pencairan Dana</TextCustom>
</Grid.Col>
<Grid.Col span={6}>
<TextCustom bold color="yellow">
{data?.akumulasiPencairan} kali
</TextCustom>
<TextCustom size="small">Akumulasi Pencairan</TextCustom>
</Grid.Col>
</Grid>
</BaseBox>
</View>
);
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
ListHeaderComponent={ListHeaderComponent}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
hideFooter
/>
);
}

View File

@@ -0,0 +1,98 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BaseBox,
Grid,
LoaderCustom,
Spacing,
StackCustom,
TextCustom,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { usePagination } from "@/hooks/use-pagination";
import { apiDonationListOfDonaturById } from "@/service/api-client/api-donation";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import { FontAwesome6 } from "@expo/vector-icons";
import dayjs from "dayjs";
import { RefreshControl } from "react-native";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
interface Donation_ScreenListOfDonaturProps {
donationId: string;
}
export default function Donation_ScreenListOfDonatur({
donationId,
}: Donation_ScreenListOfDonaturProps) {
const pagination = usePagination({
fetchFunction: async (page) => {
return await apiDonationListOfDonaturById({
id: donationId,
page: String(page),
});
},
pageSize: PAGINATION_DEFAULT_TAKE, // Sesuaikan dengan jumlah item per halaman dari API
dependencies: [donationId],
});
const renderItem = ({ item, index }: { item: any; index: number }) => (
<BaseBox key={index}>
<Grid>
<Grid.Col
span={3}
style={{ alignItems: "center", justifyContent: "center" }}
>
<FontAwesome6
name="face-smile-wink"
size={50}
style={{ color: MainColor.yellow }}
/>
</Grid.Col>
<Grid.Col span={9}>
<TextCustom bold size="large">
{item?.Author?.username || "-"}
</TextCustom>
<Spacing />
<StackCustom gap={"xs"}>
<TextCustom size={"small"}>Berdonas sebesar </TextCustom>
<TextCustom bold size="large" color="yellow">
Rp. {formatCurrencyDisplay(item?.nominal)}
</TextCustom>
<TextCustom>
{dayjs(item?.createdAt).format("DD MMM YYYY, HH:mm")}
</TextCustom>
</StackCustom>
</Grid.Col>
</Grid>
</BaseBox>
);
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
isInitialLoad: pagination.isInitialLoad,
emptyMessage: "Belum ada donatur",
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 120,
});
return (
<NewWrapper
hideFooter
listData={pagination.listData}
renderItem={renderItem}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
/>
);
}

View File

@@ -0,0 +1,97 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { BackButton, DrawerCustom, MenuDrawerDynamicGrid } from "@/components";
import { IconPlus } from "@/components/_Icon";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import { apiDonationGetNewsById } from "@/service/api-client/api-donation";
import { router, Stack } from "expo-router";
import { useState } from "react";
import { RefreshControl } from "react-native";
import Donation_BoxNews from "./BoxNews";
interface Donation_ScreenListOfNewsProps {
donationId: string;
}
export default function Donation_ScreenListOfNews({
donationId,
}: Donation_ScreenListOfNewsProps) {
const [openDrawer, setOpenDrawer] = useState(false);
const pagination = usePagination({
fetchFunction: async (page) => {
return await apiDonationGetNewsById({
id: donationId,
category: "get-all",
page: String(page),
});
},
pageSize: PAGINATION_DEFAULT_TAKE, // Sesuaikan dengan jumlah item per halaman dari API
dependencies: [donationId],
});
const renderItem = ({ item, index }: { item: any; index: number }) => (
<Donation_BoxNews key={index} item={item} />
);
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
isInitialLoad: pagination.isInitialLoad,
emptyMessage: "Tidak ada kabar",
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 80,
});
return (
<>
<Stack.Screen
options={{
title: "Daftar Kabar",
headerLeft: () => <BackButton />,
}}
/>
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
/>
}
/>
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
icon: <IconPlus />,
label: "Tambah Berita",
path: `/donation/${donationId}/(news)/add-news`,
},
]}
onPressItem={(item) => {
console.log("PATH ", item.path);
router.navigate(item.path as any);
setOpenDrawer(false);
}}
/>
</DrawerCustom>
</>
);
}

View File

@@ -0,0 +1,152 @@
import {
BadgeCustom,
BaseBox,
DummyLandscapeImage,
Grid,
StackCustom,
TextCustom,
} from "@/components";
import FloatingButton from "@/components/Button/FloatingButton";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { useAuth } from "@/hooks/use-auth";
import { usePagination } from "@/hooks/use-pagination";
import { apiDonationGetAll } from "@/service/api-client/api-donation";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import { Href, router } from "expo-router";
import _ from "lodash";
import { RefreshControl, View } from "react-native";
export default function Donation_ScreenMyDonation() {
const { user } = useAuth();
const pagination = usePagination({
fetchFunction: async (page, search) => {
if (!user?.id) {
throw new Error("User tidak ditemukan");
}
return await apiDonationGetAll({
category: "my-donation",
authorId: user?.id,
page: String(page),
});
},
pageSize: PAGINATION_DEFAULT_TAKE, // Sesuaikan dengan jumlah item per halaman yang diinginkan
dependencies: [user?.id], // Reload ketika user.id berubah
});
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
isInitialLoad: pagination.isInitialLoad,
emptyMessage: "Belum ada transaksi",
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 150,
});
const handlerColor = (status: string) => {
if (status === "menunggu") {
return "orange";
} else if (status === "proses") {
return "white";
} else if (status === "berhasil") {
return "green";
} else if (status === "gagal") {
return "red";
}
};
const handlePress = ({
invoiceId,
donationId,
status,
}: {
invoiceId: string;
donationId: string;
status: string;
}) => {
const url: Href = `../${donationId}/(transaction-flow)/${invoiceId}`;
if (status === "menunggu") {
router.push(`${url}/invoice`);
} else if (status === "proses") {
router.push(`${url}/process`);
} else if (status === "berhasil") {
router.push(`${url}/success`);
} else if (status === "gagal") {
router.push(`${url}/failed`);
}
};
const renderItem = ({ item }: { item: any }) => (
<BaseBox
paddingTop={7}
paddingBottom={7}
onPress={() => {
handlePress({
status: _.lowerCase(item.statusInvoice),
invoiceId: item.id,
donationId: item.donasiId,
});
}}
>
<Grid>
<Grid.Col span={5}>
<DummyLandscapeImage
height={100}
unClickPath
imageId={item.imageId}
/>
</Grid.Col>
<Grid.Col span={1}>
<View />
</Grid.Col>
<Grid.Col span={6}>
<StackCustom>
<TextCustom truncate={2} bold>
{item.title || "-"}
</TextCustom>
<TextCustom bold color="yellow">
Rp. {formatCurrencyDisplay(item.nominal)}
</TextCustom>
<BadgeCustom
variant="light"
color={handlerColor(_.lowerCase(item.statusInvoice))}
fullWidth
>
{item.statusInvoice}
</BadgeCustom>
</StackCustom>
</Grid.Col>
</Grid>
</BaseBox>
);
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
/>
}
hideFooter
floatingButton={
<FloatingButton onPress={() => router.push("/donation/create")} />
}
/>
);
}

View File

@@ -0,0 +1,105 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BackButton,
DotButton,
DrawerCustom,
MenuDrawerDynamicGrid,
} from "@/components";
import { IconPlus } from "@/components/_Icon";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import { apiDonationGetNewsById } from "@/service/api-client/api-donation";
import { router, Stack, useFocusEffect } from "expo-router";
import { useCallback, useState } from "react";
import { RefreshControl } from "react-native";
import Donation_BoxNews from "./BoxNews";
interface Donation_ScreenRecapOfNewsProps {
donationId: string;
}
export default function Donation_ScreenRecapOfNews({
donationId,
}: Donation_ScreenRecapOfNewsProps) {
const [openDrawer, setOpenDrawer] = useState(false);
const pagination = usePagination({
fetchFunction: async (page) => {
return await apiDonationGetNewsById({
id: donationId,
category: "get-all",
page: String(page),
});
},
pageSize: PAGINATION_DEFAULT_TAKE, // Sesuaikan dengan jumlah item per halaman dari API
dependencies: [donationId],
});
useFocusEffect(
useCallback(() => {
pagination.onRefresh();
}, [donationId]),
);
const renderItem = ({ item, index }: { item: any; index: number }) => (
<Donation_BoxNews key={index} item={item} />
);
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
isInitialLoad: pagination.isInitialLoad,
emptyMessage: "Tidak ada kabar",
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 80,
});
return (
<>
<Stack.Screen
options={{
title: "Rekap Kabar",
headerLeft: () => <BackButton />,
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
}}
/>
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
/>
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
icon: <IconPlus />,
label: "Tambah Berita",
path: `/donation/${donationId}/(news)/add-news`,
},
]}
onPressItem={(item) => {
console.log("PATH ", item.path);
router.navigate(item.path as any);
setOpenDrawer(false);
}}
/>
</DrawerCustom>
</>
);
}

View File

@@ -1,28 +1,29 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
LoaderCustom,
ScrollableCustom,
TextCustom,
} from "@/components";
import { ScrollableCustom } from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { useAuth } from "@/hooks/use-auth";
import { usePagination } from "@/hooks/use-pagination";
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
import Donasi_BoxStatus from "@/screens/Donation/BoxStatus";
import { usePagination } from "@/hooks/use-pagination";
import { apiDonationGetByStatus } from "@/service/api-client/api-donation";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { RefreshControl } from "react-native";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
interface DonationStatusProps {
initialStatus?: string;
}
export default function Donation_ScreenStatus({ initialStatus = "publish" }: DonationStatusProps) {
export default function Donation_ScreenStatus({
initialStatus = "publish",
}: DonationStatusProps) {
const { user } = useAuth();
const [activeCategory, setActiveCategory] = useState<string | null>(initialStatus);
const [activeCategory, setActiveCategory] = useState<string | null>(
initialStatus,
);
const pagination = usePagination({
fetchFunction: async (page) => {
@@ -32,18 +33,19 @@ export default function Donation_ScreenStatus({ initialStatus = "publish" }: Don
page: String(page),
});
},
pageSize: 5, // Sesuaikan dengan jumlah item per halaman dari API
pageSize: PAGINATION_DEFAULT_TAKE, // Sesuaikan dengan jumlah item per halaman dari API
dependencies: [user?.id, activeCategory],
});
useFocusEffect(
useCallback(() => {
pagination.onRefresh();
}, [user?.id, activeCategory])
}, [user?.id, activeCategory]),
);
const handlePress = (item: any) => {
setActiveCategory(item.value);
pagination.reset();
};
const scrollComponent = (
@@ -59,21 +61,19 @@ export default function Donation_ScreenStatus({ initialStatus = "publish" }: Don
);
const renderItem = ({ item, index }: { item: any; index: number }) => (
<Donasi_BoxStatus
data={item}
status={activeCategory as string}
/>
<Donasi_BoxStatus data={item} status={activeCategory as string} />
);
const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
isInitialLoad: pagination.isInitialLoad,
emptyMessage: `Tidak ada data ${activeCategory}`,
skeletonCount: 5,
skeletonHeight: 200,
});
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
isInitialLoad: pagination.isInitialLoad,
emptyMessage: `Tidak ada data ${activeCategory}`,
skeletonCount: 5,
skeletonHeight: 120,
});
return (
<NewWrapper
@@ -83,13 +83,15 @@ export default function Donation_ScreenStatus({ initialStatus = "publish" }: Don
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
/>
}
hideFooter
headerComponent={scrollComponent}
/>
);
}
}

View File

@@ -52,15 +52,18 @@ export async function apiAdminDonationUpdateStatus({
export async function apiAdminDonationListOfDonatur({
id,
status,
page = "1"
}: {
id: string;
status: "berhasil" | "gagal" | "proses" | "menunggu" | null;
page?: string;
}) {
const query = status && status !== null ? `?status=${status}` : "";
const statusQuery = status && status !== null ? `&status=${status}` : "";
const pageQuery = `&page=${page}`;
try {
const response = await apiConfig.get(
`/mobile/admin/donation/${id}/donatur${query}`
`/mobile/admin/donation/${id}/donatur?${statusQuery}${pageQuery}`
);
return response.data;
} catch (error) {
@@ -105,19 +108,6 @@ export async function apiAdminDonationInvoiceUpdateById({
}
}
export async function apiAdminDonationListOfDonaturById({
id,
}: {
id: string;
}) {
try {
const response = await apiConfig.get(`/mobile/donation/${id}/donatur`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiAdminDonationDisbursementOfFundsCreated({
id,
data,

View File

@@ -109,14 +109,17 @@ export async function apiDonationUpdateData({
export async function apiDonationGetAll({
category,
authorId,
page = "1"
}: {
category: "beranda" | "my-donation";
authorId?: string;
page?: string;
}) {
const authorQuery = authorId ? `&authorId=${authorId}` : "";
const pageQuery = `&page=${page}`;
try {
const response = await apiConfig.get(
`/mobile/donation?category=${category}${authorQuery}`
`/mobile/donation?category=${category}${authorQuery}${pageQuery}`
);
return response.data;
} catch (error) {
@@ -219,13 +222,15 @@ export async function apiDonationCreateNews({
export async function apiDonationGetNewsById({
id,
category,
page = "1"
}: {
id: string;
category: "get-all" | "get-one";
page?: string;
}) {
try {
const response = await apiConfig.get(
`/mobile/donation/${id}/news?category=${category}`
`/mobile/donation/${id}/news?category=${category}&page=${page}`
);
return response.data;
} catch (error) {
@@ -261,11 +266,28 @@ export async function apiDonationDeleteNews({ id }: { id: string }) {
export async function apiDonationDisbursementOfFundsListById({
id,
page = "1"
}: {
id: string;
page?: string;
}) {
try {
const response = await apiConfig.get(`/mobile/donation/${id}/disbursement`);
const response = await apiConfig.get(`/mobile/donation/${id}/disbursement?page=${page}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiDonationListOfDonaturById({
id,
page = "1"
}: {
id: string;
page?: string;
}) {
try {
const response = await apiConfig.get(`/mobile/donation/${id}/donatur?page=${page}`);
return response.data;
} catch (error) {
throw error;

View File

@@ -31,5 +31,5 @@ export const formatChatTime = (date: string | Date): string => {
}
// Lebih dari 7 hari lalu
return messageDate.format('DD - MM - YYYY, HH.mm'); // "05 - 11 - 2025, 14.00"
return messageDate.format('DD/MM/YYYY, HH.mm'); // "05/11/2025, 14.00"
};