feat: implement pagination and NewWrapper on donation and investment screens

- Implement pagination on investment screens (ScreenMyHolding, ScreenInvestor, ScreenRecapOfNews, ScreenListOfNews)
- Implement pagination on donation screens (ScreenStatus)
- Update API functions to support pagination with page parameter (apiInvestmentGetAll, apiInvestmentGetInvestorById, apiInvestmentGetNews, apiDonationGetByStatus)
- Replace ViewWrapper with NewWrapper for better UI experience
- Update app directory files to use new modular components from screens directory
- Add pull-to-refresh and infinite scroll functionality
- Improve performance by loading data incrementally
- Apply NewWrapper to donation create and create-story screens

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
2026-02-09 17:35:54 +08:00
parent 38a6b424e8
commit 2705f96b01
12 changed files with 185 additions and 136 deletions

View File

@@ -1,82 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
LoaderCustom,
ScrollableCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
import Donasi_BoxStatus from "@/screens/Donation/BoxStatus";
import { apiDonationGetByStatus } from "@/service/api-client/api-donation";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import Donation_ScreenStatus from "@/screens/Donation/ScreenStatus";
import { useLocalSearchParams } from "expo-router";
export default function DonationStatus() {
const { user } = useAuth();
const { status } = useLocalSearchParams<{ status?: string }>();
const [activeCategory, setActiveCategory] = useState<string | null>(
status || "publish",
);
const [listData, setListData] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadList();
}, [activeCategory]),
);
const onLoadList = async () => {
try {
setLoadList(true);
const response = await apiDonationGetByStatus({
authorId: user?.id as string,
status: activeCategory as string,
});
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
setListData(null);
} finally {
setLoadList(false);
}
};
const handlePress = (item: any) => {
setActiveCategory(item.value);
// tambahkan logika lain seperti filter dsb.
};
const scrollComponent = (
<ScrollableCustom
data={dummyMasterStatus.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as any}
/>
);
return (
<ViewWrapper hideFooter headerComponent={scrollComponent}>
{loadList ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada data {activeCategory}</TextCustom>
) : (
listData?.map((item: any, index: number) => (
<Donasi_BoxStatus
key={index}
data={item}
status={activeCategory as string}
/>
))
)}
</ViewWrapper>
<Donation_ScreenStatus initialStatus={status || "publish"} />
);
}

View File

@@ -37,7 +37,7 @@ export default function DonasiDetailStatus() {
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
}, [id]),
);
const onLoadData = async () => {
@@ -99,6 +99,7 @@ export default function DonasiDetailStatus() {
sisaHari={value.sisa}
reminder={value.reminder}
data={data}
showSisaHari={status === "publish" ? true : false}
bottomSection={
status === "publish" && (
<Donation_ProgressSection

View File

@@ -1,10 +1,12 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
InformationBox,
LandscapeFrameUploaded,
LoaderCustom,
NewWrapper,
SelectCustom,
Spacing,
StackCustom,
@@ -60,7 +62,7 @@ export default function DonationEdit() {
useCallback(() => {
onLoadData();
onLoadList();
}, [id])
}, [id]),
);
const onLoadData = async () => {
@@ -79,7 +81,6 @@ export default function DonationEdit() {
imageId: response.data.imageId,
});
}
} catch (error) {
console.log("[ERROR]", error);
}
@@ -182,7 +183,21 @@ export default function DonationEdit() {
};
return (
<ViewWrapper>
<NewWrapper
hideFooter
footerComponent={
<BoxButtonOnFooter>
<ButtonCustom
isLoading={isLoading}
onPress={() => {
handlerSubmitUpdate();
}}
>
Update
</ButtonCustom>
</BoxButtonOnFooter>
}
>
<InformationBox text="Lengkapi semua data di bawah untuk selanjutnya mengisi cerita penggalangan dana." />
{!data || loadList ? (
<LoaderCustom />
@@ -260,17 +275,9 @@ export default function DonationEdit() {
/>
<Spacing />
<ButtonCustom
isLoading={isLoading}
onPress={() => {
handlerSubmitUpdate();
}}
>
Update
</ButtonCustom>
</StackCustom>
)}
<Spacing />
</ViewWrapper>
</NewWrapper>
);
}

View File

@@ -1,5 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
InformationBox,
@@ -8,8 +9,8 @@ import {
StackCustom,
TextAreaCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import DIRECTORY_ID from "@/constants/directory-id";
import { useAuth } from "@/hooks/use-auth";
import {
@@ -112,7 +113,23 @@ export default function DonationCreateStory() {
};
return (
<ViewWrapper>
<NewWrapper
hideFooter
footerComponent={
<>
<BoxButtonOnFooter>
<ButtonCustom
isLoading={isLoading}
onPress={() => {
handlerSubmit();
}}
>
Simpan
</ButtonCustom>
</BoxButtonOnFooter>
</>
}
>
<StackCustom gap={"xs"}>
<InformationBox text="Cerita Anda adalah kunci untuk menginspirasi kebaikan. Jelaskan dengan jujur dan jelas tujuan penggalangan dana ini agar calon donatur memahami dampak positif yang dapat mereka wujudkan melalui kontribusi mereka." />
<TextAreaCustom
@@ -166,18 +183,8 @@ export default function DonationCreateStory() {
value={data.rekening}
onChangeText={(value) => setData({ ...data, rekening: value })}
/>
<Spacing />
<ButtonCustom
isLoading={isLoading}
onPress={() => {
handlerSubmit();
}}
>
Simpan
</ButtonCustom>
</StackCustom>
<Spacing />
</ViewWrapper>
</NewWrapper>
);
}

View File

@@ -1,4 +1,5 @@
import {
BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom,
InformationBox,
@@ -8,8 +9,8 @@ import {
Spacing,
StackCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import DIRECTORY_ID from "@/constants/directory-id";
import { apiDonationCreate } from "@/service/api-client/api-donation";
import { apiMasterDonation } from "@/service/api-client/api-master";
@@ -43,7 +44,7 @@ export default function DonationCreate() {
useFocusEffect(
useCallback(() => {
onLoadList();
}, [])
}, []),
);
const onLoadList = async () => {
@@ -125,7 +126,24 @@ export default function DonationCreate() {
};
return (
<ViewWrapper>
<NewWrapper
hideFooter
footerComponent={
<>
<BoxButtonOnFooter>
<ButtonCustom
isLoading={isLoading}
onPress={() => {
handlerSubmit();
// router.push(`/donation/create-story?id=${"dasdsadsa"}`);
}}
>
Selanjutnya
</ButtonCustom>
</BoxButtonOnFooter>
</>
}
>
<StackCustom gap={"xs"}>
<InformationBox text="Lengkapi semua data di bawah untuk selanjutnya mengisi cerita penggalangan dana." />
@@ -201,20 +219,8 @@ export default function DonationCreate() {
onChange={(value: any) => setData({ ...data, durasiId: value })}
/>
)}
<Spacing />
<ButtonCustom
isLoading={isLoading}
onPress={() => {
handlerSubmit();
// router.push(`/donation/create-story?id=${"dasdsadsa"}`);
}}
>
Selanjutnya
</ButtonCustom>
<Spacing />
</StackCustom>
<Spacing />
</ViewWrapper>
</NewWrapper>
);
}

View File

@@ -73,7 +73,6 @@ export default function InvestmentDetailStatus() {
updateCountDown();
}, [data]);
console.log("[DATA DETAIL]", JSON.stringify(data, null, 2));
const updateCountDown = () => {
const countDown = countDownAndCondition({

View File

@@ -72,7 +72,6 @@ export default function InvestmentDetail() {
updateCountDown();
}, [data]);
console.log("[DATA DETAIL]", JSON.stringify(data, null, 2));
const updateCountDown = () => {
const countDown = countDownAndCondition({

View File

@@ -1,14 +1,14 @@
<!-- ===================== Start Penerapan Pagination ===================== -->
File source: app/(application)/(user)/investment/[id]/(news)/list-of-news.tsx
Folder tujuan: screens/Invesment
Nama file utama: ScreenListOfNews.tsx
File source: app/(application)/(user)/donation/(tabs)/status.tsx
Folder tujuan: screens/Donation
Nama file utama: ScreenStatus.tsx
Buat file baru pada "Folder tujuan" dengan nama "Nama file utama" dan ubah nama function menjadi "Investment_ScreenListOfNews" 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_ScreenStatus" kemudian clean code, import dan panggil function tersebut pada file "File source"
Selanjutnya terapkan pagination pada file "Nama file utama"
Function fecth: apiInvestmentGetNews
File function fetch: service/api-client/api-investment.ts
Function fecth: apiDonationGetByStatus
File function fetch: service/api-client/api-donation.ts
File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
Terapkan pagination pada file "Nama file utama"
@@ -19,7 +19,6 @@ Komponen pagination yang digunaka berada pada file hooks/use-pagination.tsx dan
Perbaiki fetch "Function fecth" , pada file "File function fetch"
Jika tidak ada props page maka tambahkan props page dan default page: "1"
Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
<!-- Additional Prompt -->
@@ -47,9 +46,9 @@ Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
<!-- ===================== End Penerapan NewWrapper ===================== -->
<!-- Start Penerapan NewWrapper -->
Terapkan NewWrapper pada file: screens/Forum/DetailForum.tsx
Component yang digunakan: components/_ShareComponent/NewWrapper.tsx , karena ini adalah halaman detail saya ingin anda fokus pada props pada NewWrapper. Seperti
<!-- -->
Terapkan NewWrapper pada file: app/(application)/(user)/donation/create.tsx
Component yang digunakan: components/_ShareComponent/NewWrapper.tsx
<!-- End Penerapan NewWrapper -->
Bantu saya untuk memperbaiki logika path yang ada di dalam file "screens/Admin/Notification-Admin/ScreenNotificationAdmin2.tsx" , pada function fixPath
Saya ingin jika didalam deeplink ada "/admin/..." contoh "/admin/event/review/status" maka path yang akan di redirect adalah "/admin/event/review/status"

View File

@@ -16,6 +16,9 @@ export default function Donation_ButtonStatusSection({
}) {
const [isLoading, setLoading] = useState(false);
const [isLoadingDelete, setLoadingDelete] = useState(false);
const path: any = (status: string) => {
return `/donation/(tabs)/status?status=${status}`;
};
const handleBatalkanReview = async () => {
AlertDefaultSystem({
title: "Batalkan Review",
@@ -43,7 +46,7 @@ export default function Donation_ButtonStatusSection({
text1: response.message,
});
router.back();
router.push(path("draft"));
} catch (error) {
console.log("[ERROR]", error);
} finally {
@@ -80,7 +83,7 @@ export default function Donation_ButtonStatusSection({
text1: response.message,
});
router.back();
router.replace(path("review"));
} catch (error) {
console.log("[ERROR]", error);
} finally {
@@ -117,7 +120,7 @@ export default function Donation_ButtonStatusSection({
text1: response.message,
});
router.back();
router.replace(path("draft"));
} catch (error) {
console.log("[ERROR]", error);
} finally {

View File

@@ -12,11 +12,13 @@ import { View } from "react-native";
export default function Donation_ComponentBoxDetailData({
bottomSection,
data,
showSisaHari = true,
sisaHari,
reminder,
}: {
bottomSection?: React.ReactNode;
data: any;
showSisaHari?: boolean;
sisaHari: number;
reminder: boolean;
}) {
@@ -34,9 +36,9 @@ export default function Donation_ComponentBoxDetailData({
<TextCustom bold color="red">
Waktu berakhir
</TextCustom>
) : (
) : showSisaHari ? (
<TextCustom>Sisa hari: {sisaHari}</TextCustom>
)}
) : null}
</View>
<Grid>

View File

@@ -0,0 +1,95 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
LoaderCustom,
ScrollableCustom,
TextCustom,
} from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { useAuth } from "@/hooks/use-auth";
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) {
const { user } = useAuth();
const [activeCategory, setActiveCategory] = useState<string | null>(initialStatus);
const pagination = usePagination({
fetchFunction: async (page) => {
return await apiDonationGetByStatus({
authorId: user?.id as string,
status: activeCategory as string,
page: String(page),
});
},
pageSize: 5, // Sesuaikan dengan jumlah item per halaman dari API
dependencies: [user?.id, activeCategory],
});
useFocusEffect(
useCallback(() => {
pagination.onRefresh();
}, [user?.id, activeCategory])
);
const handlePress = (item: any) => {
setActiveCategory(item.value);
};
const scrollComponent = (
<ScrollableCustom
data={dummyMasterStatus.map((e, i) => ({
id: i,
label: e.label,
value: e.value,
}))}
onButtonPress={handlePress}
activeId={activeCategory as any}
/>
);
const renderItem = ({ item, index }: { item: any; index: number }) => (
<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,
});
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
hideFooter
headerComponent={scrollComponent}
/>
);
}

View File

@@ -40,16 +40,19 @@ export async function apiDonationGetOne({
export async function apiDonationGetByStatus({
authorId,
status,
page = "1",
}: {
authorId: string;
status: string;
page?: string;
}) {
const authorQuery = `/${authorId}`;
const statusQuery = `/${status}`;
const pageQuery = `?page=${page}`;
try {
const response = await apiConfig.get(
`/mobile/donation${authorQuery}${statusQuery}`
`/mobile/donation${authorQuery}${statusQuery}${pageQuery}`
);
return response.data;
} catch (error) {