Compare commits
21 Commits
notificati
...
loaddata/5
| Author | SHA1 | Date | |
|---|---|---|---|
| c570a19d84 | |||
| 7415c8c8ce | |||
| 72a3d42013 | |||
| d0abd14047 | |||
| 5b2be20469 | |||
| 60177a1087 | |||
| 771ae45f26 | |||
| 41f4a8ac99 | |||
| 48196cd46b | |||
| ec79a1fbcd | |||
| ed16f1b204 | |||
| d693550a1f | |||
| b3bfbc0f7e | |||
| 71e45d06cc | |||
| 07e64c335e | |||
| 1aebc9b4e8 | |||
| 5665dc88ba | |||
| da82a02a45 | |||
| 14c0f0e499 | |||
| 0262423c50 | |||
| c2682246d6 |
@@ -21,7 +21,7 @@ export default {
|
||||
"Aplikasi membutuhkan akses lokasi untuk menampilkan peta.",
|
||||
},
|
||||
associatedDomains: ["applinks:cld-dkr-staging-hipmi.wibudev.com"],
|
||||
buildNumber: "19",
|
||||
buildNumber: "20",
|
||||
},
|
||||
|
||||
android: {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { CenterCustom, TextCustom, ViewWrapper } from "@/components";
|
||||
import API_STRORAGE from "@/constants/base-url-api-strorage";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { Image } from "expo-image";
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
import React, { useState } from "react";
|
||||
import { View } from "react-native";
|
||||
|
||||
export default function PreviewImage() {
|
||||
const { id } = useLocalSearchParams();
|
||||
@@ -11,18 +13,48 @@ export default function PreviewImage() {
|
||||
return (
|
||||
<ViewWrapper>
|
||||
{id ? (
|
||||
<Image
|
||||
onLoad={() => {
|
||||
setIsLoading(false);
|
||||
<View
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "relative",
|
||||
}}
|
||||
source={
|
||||
isLoading
|
||||
? require("@/assets/images/loading.gif")
|
||||
: API_STRORAGE.GET({ fileId: id as string })
|
||||
}
|
||||
contentFit="contain"
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
/>
|
||||
>
|
||||
{/* Main Image */}
|
||||
<Image
|
||||
onLoad={() => {
|
||||
setIsLoading(false);
|
||||
}}
|
||||
source={API_STRORAGE.GET({ fileId: id as string })}
|
||||
contentFit="contain"
|
||||
style={{ width: "100%", height: "100%" }}
|
||||
// placeholder={require("@/assets/images/loading.gif")}
|
||||
/>
|
||||
|
||||
{/* Custom Loader Overlay */}
|
||||
{isLoading && (
|
||||
<View
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: MainColor.darkblue,
|
||||
zIndex: 1,
|
||||
opacity: 0.5,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
source={require("@/assets/images/loading.gif")}
|
||||
contentFit="contain"
|
||||
style={{ width: 60, height: 60 }}
|
||||
/>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<CenterCustom>
|
||||
<TextCustom>File not found</TextCustom>
|
||||
|
||||
@@ -616,6 +616,20 @@ export default function UserLayout() {
|
||||
headerLeft: () => <BackButton />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="forum/[id]/preview-report-posting"
|
||||
options={{
|
||||
title: "Laporan Postingan",
|
||||
headerLeft: () => <BackButton />,
|
||||
}}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="forum/[id]/preview-report-comment"
|
||||
options={{
|
||||
title: "Laporan Komentar",
|
||||
headerLeft: () => <BackButton />,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* ========== Maps Section ========= */}
|
||||
<Stack.Screen
|
||||
|
||||
@@ -26,7 +26,6 @@ export default function DonationBeranda() {
|
||||
const response = await apiDonationGetAll({
|
||||
category: "beranda"
|
||||
});
|
||||
console.log("[RES GET ALL]", JSON.stringify(response.data, null, 2));
|
||||
|
||||
setList(response.data);
|
||||
} catch (error) {
|
||||
|
||||
@@ -16,6 +16,7 @@ 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";
|
||||
|
||||
export default function DonationMyDonation() {
|
||||
const { user } = useAuth();
|
||||
@@ -25,20 +26,25 @@ export default function DonationMyDonation() {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [user?.id])
|
||||
}, [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,
|
||||
});
|
||||
console.log(
|
||||
"[RES GET MY DONATION]",
|
||||
JSON.stringify(response.data, null, 2)
|
||||
);
|
||||
|
||||
|
||||
setList(response.data);
|
||||
} catch (error) {
|
||||
|
||||
@@ -9,14 +9,16 @@ 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 } from "expo-router";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
export default function DonationStatus() {
|
||||
const { user } = useAuth();
|
||||
const { status } = useLocalSearchParams<{ status?: string }>();
|
||||
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>(
|
||||
"publish"
|
||||
status || "publish",
|
||||
);
|
||||
const [listData, setListData] = useState<any[] | null>(null);
|
||||
const [loadList, setLoadList] = useState(false);
|
||||
@@ -24,7 +26,7 @@ export default function DonationStatus() {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadList();
|
||||
}, [activeCategory])
|
||||
}, [activeCategory]),
|
||||
);
|
||||
|
||||
const onLoadList = async () => {
|
||||
|
||||
@@ -10,21 +10,32 @@ import {
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
||||
import { LOCAL_STORAGE_KEY } from "@/constants/local-storage-key";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import AsyncStorage from "@react-native-async-storage/async-storage";
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export default function InvestmentInputDonation() {
|
||||
const { user } = useAuth();
|
||||
const { id } = useLocalSearchParams();
|
||||
const [nominal, setNominal] = useState<number>(0);
|
||||
|
||||
const handlerSubmit = async () => {
|
||||
if (!user?.id) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "User tidak ditemukan",
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await AsyncStorage.setItem(
|
||||
LOCAL_STORAGE_KEY.transactionDonation,
|
||||
JSON.stringify({ nominal: nominal.toString() })
|
||||
JSON.stringify({ nominal: nominal.toString() }),
|
||||
);
|
||||
router.replace(`/donation/${id}/select-bank`);
|
||||
} catch (error) {
|
||||
|
||||
@@ -103,7 +103,7 @@ export default function DonationCreateStory() {
|
||||
type: "success",
|
||||
text1: "Donasi berhasil disimpan",
|
||||
});
|
||||
router.replace("/donation/status");
|
||||
router.replace("/donation/status?status=review");
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
|
||||
@@ -1,115 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
AvatarUsernameAndOtherComponent,
|
||||
BoxWithHeaderSection,
|
||||
LoaderCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper
|
||||
} from "@/components";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import {
|
||||
apiEventGetAll
|
||||
} from "@/service/api-client/api-event";
|
||||
import { dateTimeView } from "@/utils/dateTimeView";
|
||||
import { useFocusEffect } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import Event_ScreenContribution from "@/screens/Event/ScreenContribution";
|
||||
|
||||
export default function EventContribution() {
|
||||
const { user } = useAuth();
|
||||
const [listData, setListData] = useState<any>([]);
|
||||
const [isLoadList, setIsLoadList] = useState(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [user?.id])
|
||||
);
|
||||
|
||||
async function onLoadData() {
|
||||
try {
|
||||
setIsLoadList(true);
|
||||
const response = await apiEventGetAll({
|
||||
category: "contribution",
|
||||
userId: user?.id,
|
||||
});
|
||||
console.log("[DATA] ", JSON.stringify(response.data, null, 2));
|
||||
if (response.success) {
|
||||
setListData(response.data);
|
||||
|
||||
// const responseListParticipants = await apiEventListOfParticipants({
|
||||
// id: response?.data?.Event?.id,
|
||||
// });
|
||||
// console.log(
|
||||
// "[LIST PARTICIPANTS]",
|
||||
// JSON.stringify(responseListParticipants.data, null, 2)
|
||||
// );
|
||||
// if (responseListParticipants.success) {
|
||||
// setListParticipants(responseListParticipants.data);
|
||||
// }
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setIsLoadList(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<ViewWrapper hideFooter>
|
||||
{isLoadList ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<TextCustom align="center">Belum ada kontribusi</TextCustom>
|
||||
) : (
|
||||
listData.map((item: any, index: number) => (
|
||||
<BoxWithHeaderSection
|
||||
key={index}
|
||||
href={`/event/${item?.Event?.id}/contribution`}
|
||||
>
|
||||
<StackCustom>
|
||||
<AvatarUsernameAndOtherComponent
|
||||
avatar={item?.Event?.Author?.Profile?.imageId}
|
||||
avatarHref={`/profile/${item?.Event?.Author?.Profile?.id}`}
|
||||
name={item?.Event?.Author?.username}
|
||||
rightComponent={
|
||||
<TextCustom truncate>
|
||||
{dateTimeView({
|
||||
date: item?.Event?.tanggal,
|
||||
withoutTime: true,
|
||||
})}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
|
||||
<TextCustom bold align="center" size="xlarge" truncate={2}>
|
||||
{item?.Event?.title}
|
||||
</TextCustom>
|
||||
<Spacing height={0} />
|
||||
|
||||
{/* <Grid>
|
||||
{item?.Event?.Event_Peserta?.map(
|
||||
(item2: any, index2: number) => (
|
||||
<Grid.Col
|
||||
style={{ alignItems: "center" }}
|
||||
span={12 / item?.Event?.Event_Peserta?.length}
|
||||
key={index2}
|
||||
>
|
||||
<AvatarComp
|
||||
size="base"
|
||||
href={`/profile/${item2?.User?.Profile?.id}`}
|
||||
fileId={item2?.User?.Profile?.imageId}
|
||||
/>
|
||||
</Grid.Col>
|
||||
)
|
||||
)}
|
||||
</Grid> */}
|
||||
</StackCustom>
|
||||
</BoxWithHeaderSection>
|
||||
))
|
||||
)}
|
||||
</ViewWrapper>
|
||||
<>
|
||||
<Event_ScreenContribution />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,104 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { ButtonCustom, LoaderCustom, Spacing, TextCustom } from "@/components";
|
||||
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
|
||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import Event_BoxPublishSection from "@/screens/Event/BoxPublishSection";
|
||||
import { apiEventGetAll } from "@/service/api-client/api-event";
|
||||
import { dateTimeView } from "@/utils/dateTimeView";
|
||||
import _ from "lodash";
|
||||
import { useEffect, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import Event_ScreenHistory from "@/screens/Event/ScreenHistory";
|
||||
|
||||
export default function EventHistory() {
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>("all");
|
||||
const { user } = useAuth();
|
||||
const [listData, setListData] = useState<any>([]);
|
||||
const [isLoadList, setIsLoadList] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
onLoadData({ userId: user?.id });
|
||||
}, [user?.id, activeCategory]);
|
||||
|
||||
async function onLoadData({ userId }: { userId?: string }) {
|
||||
try {
|
||||
setIsLoadList(true);
|
||||
const response = await apiEventGetAll({
|
||||
category: activeCategory === "all" ? "all-history" : "my-history",
|
||||
userId: userId,
|
||||
});
|
||||
if (response.success) {
|
||||
setListData(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setIsLoadList(false);
|
||||
}
|
||||
}
|
||||
|
||||
const handlePress = (item: any) => {
|
||||
setActiveCategory(item);
|
||||
// tambahkan logika lain seperti filter dsb.
|
||||
};
|
||||
|
||||
const headerComponent = (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
padding: 5,
|
||||
backgroundColor: MainColor.soft_darkblue,
|
||||
borderRadius: 50,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<ButtonCustom
|
||||
backgroundColor={
|
||||
activeCategory === "all" ? MainColor.yellow : AccentColor.blue
|
||||
}
|
||||
textColor={activeCategory === "all" ? MainColor.black : MainColor.white}
|
||||
style={{ width: "49%" }}
|
||||
onPress={() => handlePress("all")}
|
||||
>
|
||||
Semua Riwayat
|
||||
</ButtonCustom>
|
||||
<Spacing width={"2%"} />
|
||||
<ButtonCustom
|
||||
backgroundColor={
|
||||
activeCategory === "main" ? MainColor.yellow : AccentColor.blue
|
||||
}
|
||||
textColor={
|
||||
activeCategory === "main" ? MainColor.black : MainColor.white
|
||||
}
|
||||
style={{ width: "49%" }}
|
||||
onPress={() => handlePress("main")}
|
||||
>
|
||||
Riwayat Saya
|
||||
</ButtonCustom>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<ViewWrapper headerComponent={headerComponent} hideFooter>
|
||||
{isLoadList ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<TextCustom align="center">Belum ada riwayat</TextCustom>
|
||||
) : (
|
||||
listData.map((item: any, index: number) => (
|
||||
<Event_BoxPublishSection
|
||||
key={index.toString()}
|
||||
data={item}
|
||||
rightComponentAvatar={
|
||||
<TextCustom>
|
||||
{dateTimeView({ date: item?.tanggal, withoutTime: true })}
|
||||
</TextCustom>
|
||||
}
|
||||
href={`/event/${item.id}/history`}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</ViewWrapper>
|
||||
<>
|
||||
<Event_ScreenHistory />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,63 +1,9 @@
|
||||
import { LoaderCustom, TextCustom } from "@/components";
|
||||
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
|
||||
import FloatingButton from "@/components/Button/FloatingButton";
|
||||
import Event_BoxPublishSection from "@/screens/Event/BoxPublishSection";
|
||||
import { apiEventGetAll } from "@/service/api-client/api-event";
|
||||
import { dateTimeView } from "@/utils/dateTimeView";
|
||||
import { router, useFocusEffect } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import Event_ScreenBeranda from "@/screens/Event/ScreenBeranda";
|
||||
|
||||
export default function EventBeranda() {
|
||||
const [listData, setListData] = useState([]);
|
||||
const [isLoadData, setIsLoadData] = useState(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
setIsLoadData(true);
|
||||
const response = await apiEventGetAll({category: "beranda"});
|
||||
// console.log("Response", JSON.stringify(response.data, null, 2));
|
||||
setListData(response.data);
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setIsLoadData(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewWrapper
|
||||
hideFooter
|
||||
floatingButton={
|
||||
<FloatingButton onPress={() => router.push("/event/create")} />
|
||||
}
|
||||
>
|
||||
{isLoadData ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<TextCustom align="center">Belum ada event</TextCustom>
|
||||
) : (
|
||||
listData.map((item: any, index) => (
|
||||
|
||||
|
||||
<Event_BoxPublishSection
|
||||
key={index}
|
||||
href={`/event/${item.id}/publish`}
|
||||
data={item}
|
||||
rightComponentAvatar={
|
||||
<TextCustom>
|
||||
{dateTimeView({ date: item?.tanggal, withoutTime: true })}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</ViewWrapper>
|
||||
<>
|
||||
<Event_ScreenBeranda />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,101 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BoxWithHeaderSection,
|
||||
Grid,
|
||||
LoaderCustom,
|
||||
ScrollableCustom,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
} from "@/components";
|
||||
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
|
||||
import { apiEventGetByStatus } from "@/service/api-client/api-event";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import Event_ScreenStatus from "@/screens/Event/ScreenStatus";
|
||||
|
||||
export default function EventStatus() {
|
||||
const { user } = useAuth();
|
||||
const { status } = useLocalSearchParams<{ status?: string }>();
|
||||
|
||||
const id = user?.id || "";
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>(
|
||||
status || "publish"
|
||||
);
|
||||
const [listData, setListData] = useState([]);
|
||||
const [loadingGetData, setLoadingGetData] = useState(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [activeCategory, id])
|
||||
);
|
||||
|
||||
async function onLoadData() {
|
||||
try {
|
||||
setLoadingGetData(true);
|
||||
const response = await apiEventGetByStatus({
|
||||
id: id!,
|
||||
status: activeCategory!,
|
||||
});
|
||||
// console.log("Response", JSON.stringify(response.data, null, 2));
|
||||
setListData(response.data);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setLoadingGetData(false);
|
||||
}
|
||||
}
|
||||
|
||||
const handlePress = (item: any) => {
|
||||
setActiveCategory(item.value);
|
||||
// tambahkan logika lain seperti filter dsb.
|
||||
};
|
||||
|
||||
const tabsComponent = (
|
||||
<ScrollableCustom
|
||||
data={dummyMasterStatus.map((e, i) => ({
|
||||
id: i,
|
||||
label: e.label,
|
||||
value: e.value,
|
||||
}))}
|
||||
onButtonPress={handlePress}
|
||||
activeId={activeCategory as any}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<ViewWrapper headerComponent={tabsComponent}>
|
||||
{loadingGetData ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<TextCustom align="center">Tidak ada data {activeCategory}</TextCustom>
|
||||
) : (
|
||||
listData.map((item: any, i) => (
|
||||
<BoxWithHeaderSection
|
||||
key={i}
|
||||
href={`/event/${item.id}/${activeCategory}/detail-event`}
|
||||
>
|
||||
<StackCustom gap={"xs"}>
|
||||
<Grid>
|
||||
<Grid.Col span={8}>
|
||||
<TextCustom truncate bold>
|
||||
{item?.title}
|
||||
</TextCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
|
||||
<TextCustom>
|
||||
{new Date(item?.tanggal).toLocaleDateString()}
|
||||
</TextCustom>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
<TextCustom truncate={2}>{item?.deskripsi}</TextCustom>
|
||||
</StackCustom>
|
||||
</BoxWithHeaderSection>
|
||||
))
|
||||
)}
|
||||
</ViewWrapper>
|
||||
<>
|
||||
<Event_ScreenStatus />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCustom,
|
||||
LoaderCustom,
|
||||
NewWrapper,
|
||||
SelectCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
@@ -10,6 +12,7 @@ import {
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
|
||||
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
|
||||
import {
|
||||
apiEventGetOne,
|
||||
@@ -48,14 +51,14 @@ export default function EventEdit() {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id])
|
||||
}, [id]),
|
||||
);
|
||||
|
||||
async function onLoadData() {
|
||||
try {
|
||||
setIsLoadData(true);
|
||||
const response = await apiEventGetOne({ id: id as string });
|
||||
console.log("[DATA BY ID]", JSON.stringify(response, null, 2));
|
||||
|
||||
if (response.success) {
|
||||
setData(response.data);
|
||||
setSelectedDate(new Date(response.data.tanggal));
|
||||
@@ -100,6 +103,15 @@ export default function EventEdit() {
|
||||
const startDate = new Date(selectedDate as any);
|
||||
const endDate = new Date(selectedEndDate as any);
|
||||
|
||||
if (!startDate) {
|
||||
Toast.show({
|
||||
type: "info",
|
||||
text1: "Info",
|
||||
text2: "Tanggal mulai tidak valid",
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (startDate >= endDate) {
|
||||
Toast.show({
|
||||
type: "info",
|
||||
@@ -146,7 +158,7 @@ export default function EventEdit() {
|
||||
|
||||
const validateDateRange = (
|
||||
selectedDate: string | Date,
|
||||
selectedEndDate: string | Date
|
||||
selectedEndDate: string | Date,
|
||||
): { isValid: boolean; error?: string } => {
|
||||
const startDate = new Date(selectedDate);
|
||||
const endDate = new Date(selectedEndDate);
|
||||
@@ -174,9 +186,19 @@ export default function EventEdit() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper>
|
||||
<NewWrapper
|
||||
footerComponent={
|
||||
<BoxButtonOnFooter>
|
||||
<ButtonCustom
|
||||
isLoading={isLoading}
|
||||
title="Update"
|
||||
onPress={handlerSubmit}
|
||||
/>
|
||||
</BoxButtonOnFooter>
|
||||
}
|
||||
>
|
||||
{isLoadData ? (
|
||||
<LoaderCustom />
|
||||
<ListSkeletonComponent />
|
||||
) : (
|
||||
<StackCustom gap={"xs"}>
|
||||
<TextInputCustom
|
||||
@@ -186,26 +208,15 @@ export default function EventEdit() {
|
||||
value={data?.title}
|
||||
onChangeText={(value) => setData({ ...data, title: value })}
|
||||
/>
|
||||
<SelectCustom
|
||||
label="Tipe Event"
|
||||
placeholder="Pilih tipe event"
|
||||
data={listTypeEvent.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}))}
|
||||
value={data?.eventMaster_TipeAcaraId || ""}
|
||||
onChange={(value) => {
|
||||
console.log(value);
|
||||
setData({ ...data, eventMaster_TipeAcaraId: value });
|
||||
}}
|
||||
/>
|
||||
<TextInputCustom
|
||||
label="Lokasi"
|
||||
placeholder="Masukkan lokasi event"
|
||||
<TextAreaCustom
|
||||
label="Deskripsi"
|
||||
placeholder="Masukkan deskripsi event"
|
||||
required
|
||||
value={data?.lokasi}
|
||||
onChangeText={(value) => setData({ ...data, lokasi: value })}
|
||||
showCount
|
||||
value={data?.deskripsi}
|
||||
onChangeText={(value) => setData({ ...data, deskripsi: value })}
|
||||
/>
|
||||
|
||||
<DateTimePickerCustom
|
||||
minimumDate={new Date(Date.now())}
|
||||
label="Tanggal & Waktu Mulai"
|
||||
@@ -233,7 +244,7 @@ export default function EventEdit() {
|
||||
{
|
||||
validateDateRange(
|
||||
selectedDate as any,
|
||||
selectedEndDate as any
|
||||
selectedEndDate as any,
|
||||
).error
|
||||
}
|
||||
</TextCustom>
|
||||
@@ -242,31 +253,37 @@ export default function EventEdit() {
|
||||
{
|
||||
validateDateRange(
|
||||
selectedDate as any,
|
||||
selectedEndDate as any
|
||||
selectedEndDate as any,
|
||||
).error
|
||||
}
|
||||
</TextCustom>
|
||||
)}
|
||||
<Spacing />
|
||||
{/* <Spacing /> */}
|
||||
</StackCustom>
|
||||
|
||||
<TextAreaCustom
|
||||
label="Deskripsi"
|
||||
placeholder="Masukkan deskripsi event"
|
||||
required
|
||||
showCount
|
||||
value={data?.deskripsi}
|
||||
onChangeText={(value) => setData({ ...data, deskripsi: value })}
|
||||
<SelectCustom
|
||||
label="Tipe Event"
|
||||
placeholder="Pilih tipe event"
|
||||
data={listTypeEvent.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}))}
|
||||
value={data?.eventMaster_TipeAcaraId || ""}
|
||||
onChange={(value) => {
|
||||
console.log(value);
|
||||
setData({ ...data, eventMaster_TipeAcaraId: value });
|
||||
}}
|
||||
/>
|
||||
|
||||
<ButtonCustom
|
||||
isLoading={isLoading}
|
||||
title="Update"
|
||||
onPress={handlerSubmit}
|
||||
<TextInputCustom
|
||||
label="Lokasi"
|
||||
placeholder="Masukkan lokasi event"
|
||||
required
|
||||
value={data?.lokasi}
|
||||
onChangeText={(value) => setData({ ...data, lokasi: value })}
|
||||
/>
|
||||
</StackCustom>
|
||||
)}
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,110 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
AvatarUsernameAndOtherComponent,
|
||||
BadgeCustom,
|
||||
BaseBox,
|
||||
LoaderCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import {
|
||||
apiEventGetOne,
|
||||
apiEventListOfParticipants,
|
||||
} from "@/service/api-client/api-event";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { View } from "react-native";
|
||||
import Event_ScreenListOfParticipants from "@/screens/Event/ScreenListOfParticipants";
|
||||
|
||||
export default function EventListOfParticipants() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const [startDate, setStartDate] = useState<Dayjs | undefined>();
|
||||
const [listData, setListData] = useState<any[] | null>(null);
|
||||
const [loadtData, setLoadData] = useState(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
handlerLoadData();
|
||||
}, [id])
|
||||
);
|
||||
|
||||
const handlerLoadData = () => {
|
||||
try {
|
||||
onLoadData();
|
||||
onLoadList();
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
}
|
||||
};
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
const response = await apiEventGetOne({ id: id as string });
|
||||
if (response.success) {
|
||||
const date = dayjs(response.data.tanggal);
|
||||
setStartDate(date);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
}
|
||||
};
|
||||
|
||||
const onLoadList = async () => {
|
||||
try {
|
||||
setLoadData(true);
|
||||
const response = await apiEventListOfParticipants({ id: id as string });
|
||||
|
||||
if (response.success) {
|
||||
setListData(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setLoadData(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewWrapper>
|
||||
{loadtData && !listData ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<TextCustom align="center" color="gray">
|
||||
Belum ada peserta
|
||||
</TextCustom>
|
||||
) : (
|
||||
listData?.map((item: any, index: number) => (
|
||||
<BaseBox key={index}>
|
||||
<AvatarUsernameAndOtherComponent
|
||||
avatar={item?.User?.Profile?.imageId}
|
||||
name={item?.User?.username}
|
||||
avatarHref={`/profile/${item?.User?.Profile?.id}`}
|
||||
rightComponent={
|
||||
startDate && startDate.subtract(1, "hour").diff(dayjs()) < 0 ? (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<BadgeCustom color={item?.isPresent ? "green" : "red"}>
|
||||
{item?.isPresent ? "Hadir" : "Tidak Hadir"}
|
||||
</BadgeCustom>
|
||||
</View>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<BadgeCustom color="gray">-</BadgeCustom>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</BaseBox>
|
||||
))
|
||||
)}
|
||||
</ViewWrapper>
|
||||
<>
|
||||
<Event_ScreenListOfParticipants />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCustom,
|
||||
NewWrapper,
|
||||
SelectCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextAreaCustom,
|
||||
TextCustom,
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
@@ -100,7 +101,6 @@ export default function EventCreate() {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
const buttonSubmit = (
|
||||
<ButtonCustom
|
||||
@@ -112,7 +112,9 @@ export default function EventCreate() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper>
|
||||
<NewWrapper
|
||||
footerComponent={<BoxButtonOnFooter>{buttonSubmit}</BoxButtonOnFooter>}
|
||||
>
|
||||
<StackCustom gap={"xs"}>
|
||||
<TextInputCustom
|
||||
placeholder="Masukkan nama event"
|
||||
@@ -121,24 +123,15 @@ export default function EventCreate() {
|
||||
onChangeText={(value: any) => setData({ ...data, title: value })}
|
||||
/>
|
||||
|
||||
<SelectCustom
|
||||
label="Tipe Event"
|
||||
placeholder="Pilih tipe event"
|
||||
data={listTypeEvent.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}))}
|
||||
value={data?.eventMaster_TipeAcaraId || null}
|
||||
onChange={(value: any) =>
|
||||
setData({ ...data, eventMaster_TipeAcaraId: value })
|
||||
}
|
||||
/>
|
||||
|
||||
<TextInputCustom
|
||||
label="Lokasi"
|
||||
placeholder="Masukkan lokasi event"
|
||||
<TextAreaCustom
|
||||
label="Deskripsi"
|
||||
placeholder="Masukkan deskripsi event"
|
||||
required
|
||||
onChangeText={(value: any) => setData({ ...data, lokasi: value })}
|
||||
showCount
|
||||
value={data?.deskripsi || ""}
|
||||
onChangeText={(value: any) =>
|
||||
setData({ ...data, deskripsi: value })
|
||||
}
|
||||
/>
|
||||
|
||||
<DateTimePickerCustom
|
||||
@@ -168,22 +161,28 @@ export default function EventCreate() {
|
||||
</TextCustom>
|
||||
)}
|
||||
<Spacing />
|
||||
<SelectCustom
|
||||
label="Tipe Event"
|
||||
placeholder="Pilih tipe event"
|
||||
data={listTypeEvent.map((item: any) => ({
|
||||
label: item.name,
|
||||
value: item.id,
|
||||
}))}
|
||||
value={data?.eventMaster_TipeAcaraId || null}
|
||||
onChange={(value: any) =>
|
||||
setData({ ...data, eventMaster_TipeAcaraId: value })
|
||||
}
|
||||
/>
|
||||
|
||||
<TextInputCustom
|
||||
label="Lokasi"
|
||||
placeholder="Masukkan lokasi event"
|
||||
required
|
||||
onChangeText={(value: any) => setData({ ...data, lokasi: value })}
|
||||
/>
|
||||
</StackCustom>
|
||||
|
||||
<TextAreaCustom
|
||||
label="Deskripsi"
|
||||
placeholder="Masukkan deskripsi event"
|
||||
required
|
||||
showCount
|
||||
value={data?.deskripsi || ""}
|
||||
onChangeText={(value: any) =>
|
||||
setData({ ...data, deskripsi: value })
|
||||
}
|
||||
/>
|
||||
|
||||
{buttonSubmit}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,271 +1,11 @@
|
||||
import {
|
||||
ButtonCustom,
|
||||
DrawerCustom,
|
||||
LoaderCustom,
|
||||
Spacing,
|
||||
TextAreaCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AlertWarning from "@/components/Alert/AlertWarning";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import Forum_CommentarBoxSection from "@/screens/Forum/CommentarBoxSection";
|
||||
import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection";
|
||||
import Forum_MenuDrawerBerandaSection from "@/screens/Forum/MenuDrawerSection.tsx/MenuBeranda";
|
||||
import Forum_MenuDrawerCommentar from "@/screens/Forum/MenuDrawerSection.tsx/MenuCommentar";
|
||||
import {
|
||||
apiForumCreateComment,
|
||||
apiForumGetComment,
|
||||
apiForumGetOne,
|
||||
apiForumUpdateStatus,
|
||||
} from "@/service/api-client/api-forum";
|
||||
import { isBadContent } from "@/utils/badWordsIndonesia";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Alert } from "react-native";
|
||||
|
||||
interface CommentProps {
|
||||
id: string;
|
||||
isActive: boolean;
|
||||
komentar: string;
|
||||
createdAt: Date;
|
||||
authorId: string;
|
||||
Author: {
|
||||
id: string;
|
||||
username: string;
|
||||
Profile: {
|
||||
id: string;
|
||||
imageId: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
import DetailForum from "@/screens/Forum/DetailForum";
|
||||
import DetailForum2 from "@/screens/Forum/DetailForum2";
|
||||
|
||||
export default function ForumDetail() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const { user } = useAuth();
|
||||
const [openDrawer, setOpenDrawer] = useState(false);
|
||||
const [data, setData] = useState<any | null>(null);
|
||||
const [listComment, setListComment] = useState<CommentProps[] | null>(null);
|
||||
const [isLoadingComment, setLoadingComment] = useState(false);
|
||||
|
||||
// Status
|
||||
const [status, setStatus] = useState("");
|
||||
const [text, setText] = useState("");
|
||||
const [authorId, setAuthorId] = useState("");
|
||||
const [dataId, setDataId] = useState("");
|
||||
|
||||
// Comentar
|
||||
const [openDrawerCommentar, setOpenDrawerCommentar] = useState(false);
|
||||
const [commentId, setCommentId] = useState("");
|
||||
const [commentAuthorId, setCommentAuthorId] = useState("");
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData(id as string);
|
||||
}, [id])
|
||||
);
|
||||
|
||||
const onLoadData = async (id: string) => {
|
||||
try {
|
||||
const response = await apiForumGetOne({ id });
|
||||
setData(response.data);
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
onLoadListComment(id as string);
|
||||
}, [id]);
|
||||
|
||||
const onLoadListComment = async (id: string) => {
|
||||
try {
|
||||
const response = await apiForumGetComment({
|
||||
id: id as string,
|
||||
});
|
||||
setListComment(response.data);
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Update Status
|
||||
const handlerUpdateStatus = async (value: any) => {
|
||||
try {
|
||||
const response = await apiForumUpdateStatus({
|
||||
id: id as string,
|
||||
data: value,
|
||||
});
|
||||
if (response.success) {
|
||||
setStatus(response.data);
|
||||
setData({
|
||||
...data,
|
||||
ForumMaster_StatusPosting: {
|
||||
status: response.data,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Create Commentar
|
||||
const handlerCreateCommentar = async () => {
|
||||
if (isBadContent(text)) {
|
||||
AlertWarning({});
|
||||
return;
|
||||
}
|
||||
|
||||
const newData = {
|
||||
comment: text,
|
||||
authorId: user?.id,
|
||||
};
|
||||
try {
|
||||
setLoadingComment(true);
|
||||
const response = await apiForumCreateComment({
|
||||
id: id as string,
|
||||
data: newData,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setText("");
|
||||
const newComment = {
|
||||
id: response.data.id,
|
||||
isActive: response.data.isActive,
|
||||
komentar: response.data.komentar,
|
||||
createdAt: response.data.createdAt,
|
||||
authorId: response.data.authorId,
|
||||
Author: response.data.Author,
|
||||
};
|
||||
setListComment((prev) => [newComment, ...(prev || [])]);
|
||||
setData({
|
||||
...data,
|
||||
count: data.count + 1,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setLoadingComment(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper>
|
||||
{!data && !listComment ? (
|
||||
<LoaderCustom />
|
||||
) : (
|
||||
<>
|
||||
{/* Box Posting */}
|
||||
<Forum_BoxDetailSection
|
||||
data={data}
|
||||
onSetData={() => {
|
||||
setOpenDrawer(true);
|
||||
setStatus(data.ForumMaster_StatusPosting?.status);
|
||||
setAuthorId(data.Author?.id);
|
||||
setDataId(data.id);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Area Commentar */}
|
||||
{data?.ForumMaster_StatusPosting?.status === "Open" && (
|
||||
<>
|
||||
<TextAreaCustom
|
||||
placeholder="Ketik diskusi anda..."
|
||||
maxLength={1000}
|
||||
showCount
|
||||
value={text}
|
||||
onChangeText={setText}
|
||||
style={{
|
||||
marginBottom: 0,
|
||||
}}
|
||||
/>
|
||||
<ButtonCustom
|
||||
isLoading={isLoadingComment}
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
}}
|
||||
onPress={() => {
|
||||
handlerCreateCommentar();
|
||||
}}
|
||||
>
|
||||
Balas
|
||||
</ButtonCustom>
|
||||
</>
|
||||
)}
|
||||
<Spacing height={40} />
|
||||
|
||||
{/* List Commentar */}
|
||||
{_.isEmpty(listComment) ? (
|
||||
<TextCustom align="center" color="gray" size={"small"}>
|
||||
Tidak ada komentar
|
||||
</TextCustom>
|
||||
) : (
|
||||
<TextCustom color="gray">Komentar :</TextCustom>
|
||||
)}
|
||||
<Spacing height={5} />
|
||||
{listComment?.map((item: any, index: number) => (
|
||||
<Forum_CommentarBoxSection
|
||||
key={index}
|
||||
data={item}
|
||||
onSetData={(value) => {
|
||||
setCommentId(value.setCommentId);
|
||||
setOpenDrawerCommentar(value.setOpenDrawer);
|
||||
setCommentAuthorId(value.setCommentAuthorId);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</ViewWrapper>
|
||||
|
||||
{/* Posting Drawer */}
|
||||
<DrawerCustom
|
||||
height={"auto"}
|
||||
isVisible={openDrawer}
|
||||
closeDrawer={() => setOpenDrawer(false)}
|
||||
>
|
||||
<Forum_MenuDrawerBerandaSection
|
||||
id={dataId}
|
||||
authorUsername={data?.Author?.username as string}
|
||||
status={status}
|
||||
setIsDrawerOpen={() => {
|
||||
setOpenDrawer(false);
|
||||
}}
|
||||
authorId={authorId}
|
||||
handlerUpdateStatus={(value: any) => {
|
||||
handlerUpdateStatus(value);
|
||||
}}
|
||||
/>
|
||||
</DrawerCustom>
|
||||
|
||||
{/* Commentar Drawer */}
|
||||
<DrawerCustom
|
||||
height={"auto"}
|
||||
isVisible={openDrawerCommentar}
|
||||
closeDrawer={() => setOpenDrawerCommentar(false)}
|
||||
>
|
||||
<Forum_MenuDrawerCommentar
|
||||
id={commentId as string}
|
||||
commentId={commentId}
|
||||
commentAuthorId={commentAuthorId}
|
||||
setIsDrawerOpen={() => {
|
||||
setOpenDrawerCommentar(false);
|
||||
}}
|
||||
listComment={listComment}
|
||||
setListComment={setListComment}
|
||||
countComment={data?.count}
|
||||
setCountComment={(val: any) => {
|
||||
setData((prev: any) => ({
|
||||
...prev,
|
||||
count: val,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</DrawerCustom>
|
||||
{/* <DetailForum />; */}
|
||||
<DetailForum2 />
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from "@/components";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { apiForumCreateReportCommentar } from "@/service/api-client/api-master";
|
||||
import { apiForumCreateReportCommentar } from "@/service/api-client/api-forum";
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from "@/components";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { apiForumCreateReportPosting } from "@/service/api-client/api-master";
|
||||
import { apiForumCreateReportPosting } from "@/service/api-client/api-forum";
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import {
|
||||
BaseBox,
|
||||
NewWrapper,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
} from "@/components";
|
||||
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
|
||||
import NoDataText from "@/components/_ShareComponent/NoDataText";
|
||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||
import { apiForumGetReportComment } from "@/service/api-client/api-forum";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
export default function ForumPreviewReportComment() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const [data, setData] = useState<any | null>(null);
|
||||
const [listData, setListData] = useState<any | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
// Status
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData(id as string);
|
||||
}, [id])
|
||||
);
|
||||
|
||||
const onLoadData = async (id: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await apiForumGetReportComment({ id });
|
||||
setData(response.data);
|
||||
setListData(response?.data?.Forum_ReportKomentar);
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<NewWrapper>
|
||||
<StackCustom>
|
||||
<TextCustom color="red" bold>
|
||||
Komentar anda telah melanggar aturan forum ! Admin mengambil
|
||||
tindakan untuk menghapus komentar anda!
|
||||
</TextCustom>
|
||||
{loading ? (
|
||||
<CustomSkeleton height={100} />
|
||||
) : (
|
||||
<BaseBox>
|
||||
<TextCustom>"{data?.komentar ? data?.komentar : "-"}"</TextCustom>
|
||||
</BaseBox>
|
||||
)}
|
||||
</StackCustom>
|
||||
|
||||
<Spacing height={10} />
|
||||
<TextCustom bold>Beberapa laporan yang telah diterima</TextCustom>
|
||||
<Spacing height={10} />
|
||||
|
||||
{loading ? (
|
||||
<ListSkeletonComponent />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<NoDataText />
|
||||
) : (
|
||||
listData?.map((e: any, index: number) => (
|
||||
<BaseBox key={index}>
|
||||
{e?.deskripsi ? (
|
||||
<StackCustom gap={"sm"}>
|
||||
<TextCustom bold>Laporan Lainnya</TextCustom>
|
||||
<TextCustom>{e?.deskripsi}</TextCustom>
|
||||
</StackCustom>
|
||||
) : (
|
||||
<StackCustom gap={"sm"}>
|
||||
<TextCustom bold>
|
||||
{e?.ForumMaster_KategoriReport?.title}
|
||||
</TextCustom>
|
||||
<TextCustom>
|
||||
{e?.ForumMaster_KategoriReport?.deskripsi}
|
||||
</TextCustom>
|
||||
</StackCustom>
|
||||
)}
|
||||
</BaseBox>
|
||||
))
|
||||
)}
|
||||
</NewWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import {
|
||||
BaseBox,
|
||||
NewWrapper,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
} from "@/components";
|
||||
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
|
||||
import NoDataText from "@/components/_ShareComponent/NoDataText";
|
||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||
import { apiForumGetReportPosting } from "@/service/api-client/api-forum";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
export default function ForumPreviewReportPosting() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const [data, setData] = useState<any | null>(null);
|
||||
const [listData, setListData] = useState<any | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
// Status
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData(id as string);
|
||||
}, [id])
|
||||
);
|
||||
|
||||
const onLoadData = async (id: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await apiForumGetReportPosting({ id });
|
||||
setData(response.data);
|
||||
setListData(response?.data?.Forum_ReportPosting);
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<NewWrapper>
|
||||
<StackCustom>
|
||||
<TextCustom color="red" bold>
|
||||
Postingan anda telah melanggar aturan forum ! Admin mengambil
|
||||
tindakan untuk menghapus komentar anda!
|
||||
</TextCustom>
|
||||
{loading ? (
|
||||
<CustomSkeleton height={100} />
|
||||
) : (
|
||||
<BaseBox>
|
||||
<TextCustom>"{data?.diskusi ? data?.diskusi : "-"}"</TextCustom>
|
||||
</BaseBox>
|
||||
)}
|
||||
</StackCustom>
|
||||
|
||||
<Spacing height={10} />
|
||||
<TextCustom bold>Beberapa laporan yang telah diterima</TextCustom>
|
||||
<Spacing height={10} />
|
||||
|
||||
{loading ? (
|
||||
<ListSkeletonComponent />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<NoDataText />
|
||||
) : (
|
||||
listData?.map((e: any) => (
|
||||
<BaseBox key={e?.id}>
|
||||
{e?.deskripsi ? (
|
||||
<StackCustom gap={"sm"}>
|
||||
<TextCustom bold>Laporan Lainnya</TextCustom>
|
||||
<TextCustom>{e?.deskripsi}</TextCustom>
|
||||
</StackCustom>
|
||||
) : (
|
||||
<StackCustom gap={"sm"}>
|
||||
<TextCustom bold>
|
||||
{e?.ForumMaster_KategoriReport?.title}
|
||||
</TextCustom>
|
||||
<TextCustom>
|
||||
{e?.ForumMaster_KategoriReport?.deskripsi}
|
||||
</TextCustom>
|
||||
</StackCustom>
|
||||
)}
|
||||
</BaseBox>
|
||||
))
|
||||
)}
|
||||
</NewWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -8,7 +8,8 @@ import {
|
||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import Forum_ReportListSection from "@/screens/Forum/ReportListSection";
|
||||
import { apiForumCreateReportCommentar, apiMasterForumReportList } from "@/service/api-client/api-master";
|
||||
import { apiForumCreateReportCommentar } from "@/service/api-client/api-forum";
|
||||
import { apiMasterForumReportList } from "@/service/api-client/api-master";
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
import { useState, useEffect } from "react";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
@@ -9,8 +9,8 @@ import {
|
||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import Forum_ReportListSection from "@/screens/Forum/ReportListSection";
|
||||
import { apiForumCreateReportPosting } from "@/service/api-client/api-forum";
|
||||
import {
|
||||
apiForumCreateReportPosting,
|
||||
apiMasterForumReportList,
|
||||
} from "@/service/api-client/api-master";
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
|
||||
@@ -2,15 +2,14 @@ import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCustom,
|
||||
TextAreaCustom,
|
||||
ViewWrapper
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import AlertWarning from "@/components/Alert/AlertWarning";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { apiForumCreate } from "@/service/api-client/api-forum";
|
||||
import { isBadContent } from "@/utils/badWordsIndonesia";
|
||||
import { censorText, isBadContent } from "@/utils/badWordsIndonesia";
|
||||
import { router } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import { Alert } from "react-native";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export default function ForumCreate() {
|
||||
@@ -19,16 +18,22 @@ export default function ForumCreate() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handlerSubmit = async () => {
|
||||
|
||||
if (isBadContent(text)) {
|
||||
AlertWarning({})
|
||||
if (text.trim() === "") {
|
||||
AlertWarning({
|
||||
title: "Lengkapi Data",
|
||||
description: "Postingan tidak boleh kosong",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Bisa di sensor atau return dan tidak bisa di post
|
||||
const cencorContent = censorText(text)
|
||||
|
||||
const newData = {
|
||||
diskusi: text,
|
||||
diskusi: cencorContent,
|
||||
authorId: user?.id,
|
||||
};
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await apiForumCreate({ data: newData });
|
||||
@@ -50,6 +55,7 @@ export default function ForumCreate() {
|
||||
const buttonFooter = (
|
||||
<BoxButtonOnFooter>
|
||||
<ButtonCustom
|
||||
disabled={!text.trim() || isLoading}
|
||||
isLoading={isLoading}
|
||||
onPress={() => {
|
||||
handlerSubmit();
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import Forum_ViewBeranda from "@/screens/Forum/ViewBeranda";
|
||||
import Forum_ViewBeranda2 from "@/screens/Forum/ViewBeranda2";
|
||||
import Forum_ViewBeranda3 from "@/screens/Forum/ViewBeranda3";
|
||||
|
||||
export default function Forum() {
|
||||
return (
|
||||
<>
|
||||
{/* <Forum_ViewBeranda /> */}
|
||||
<Forum_ViewBeranda2 />
|
||||
{/* <Forum_ViewBeranda2 /> */}
|
||||
<Forum_ViewBeranda3 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,33 @@
|
||||
import BackButtonFromNotification from "@/components/Button/BackButtonFromNotification";
|
||||
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
||||
import { TabsStyles } from "@/styles/tabs-styles";
|
||||
import { Feather, FontAwesome6, Ionicons } from "@expo/vector-icons";
|
||||
import { Tabs } from "expo-router";
|
||||
import { router, Tabs, useLocalSearchParams, useNavigation } from "expo-router";
|
||||
import { useLayoutEffect } from "react";
|
||||
|
||||
export default function InvestmentTabsLayout() {
|
||||
// const navigation = useNavigation();
|
||||
|
||||
// const { from, category } = useLocalSearchParams<{
|
||||
// from?: string;
|
||||
// category?: string;
|
||||
// }>();
|
||||
|
||||
// console.log("from", from);
|
||||
// console.log("category", category);
|
||||
|
||||
// // Atur header secara dinamis
|
||||
// useLayoutEffect(() => {
|
||||
// navigation.setOptions({
|
||||
// headerLeft: () => (
|
||||
// <BackButtonFromNotification
|
||||
// from={from as string}
|
||||
// category={category as string}
|
||||
// />
|
||||
// ),
|
||||
// });
|
||||
// }, [from, router, navigation]);
|
||||
|
||||
return (
|
||||
<Tabs screenOptions={TabsStyles}>
|
||||
<Tabs.Screen
|
||||
|
||||
@@ -9,14 +9,16 @@ import { useAuth } from "@/hooks/use-auth";
|
||||
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
|
||||
import Investment_StatusBox from "@/screens/Invesment/StatusBox";
|
||||
import { apiInvestmentGetByStatus } from "@/service/api-client/api-investment";
|
||||
import { useFocusEffect } from "expo-router";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
export default function InvestmentPortofolio() {
|
||||
const { user } = useAuth();
|
||||
const { status } = useLocalSearchParams<{ status?: string }>();
|
||||
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>(
|
||||
"publish"
|
||||
status || "publish"
|
||||
);
|
||||
|
||||
const [listData, setListData] = useState<any[]>([]);
|
||||
|
||||
@@ -115,7 +115,11 @@ export default function InvestmentAddNews() {
|
||||
onChangeText={(value) => setData({ ...data, deskripsi: value })}
|
||||
/>
|
||||
|
||||
<ButtonCustom isLoading={isLoading} onPress={handlerSubmit}>
|
||||
<ButtonCustom
|
||||
disabled={!data.title || !data.deskripsi || isLoading}
|
||||
isLoading={isLoading}
|
||||
onPress={handlerSubmit}
|
||||
>
|
||||
Simpan
|
||||
</ButtonCustom>
|
||||
</StackCustom>
|
||||
|
||||
@@ -27,7 +27,6 @@ import Toast from "react-native-toast-message";
|
||||
|
||||
export default function InvestmentInvoice() {
|
||||
const { id } = useLocalSearchParams();
|
||||
console.log("[ID]", id);
|
||||
const [data, setData] = useState<any>({});
|
||||
const [image, setImage] = useState<IFileData>({
|
||||
name: "",
|
||||
@@ -49,7 +48,6 @@ export default function InvestmentInvoice() {
|
||||
category: "invoice",
|
||||
});
|
||||
|
||||
console.log("[RES INVOICE]", JSON.stringify(response.data, null, 2));
|
||||
setData(response.data);
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
@@ -64,8 +62,6 @@ export default function InvestmentInvoice() {
|
||||
imageUri: image?.uri,
|
||||
});
|
||||
|
||||
console.log("[RESPONSE UPLOAD IMAGE]", responseUploadImage);
|
||||
|
||||
if (!responseUploadImage?.data?.id) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
@@ -83,10 +79,6 @@ export default function InvestmentInvoice() {
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
console.log(
|
||||
"[RESPONSE UPDATE]",
|
||||
JSON.stringify(response.data, null, 2)
|
||||
);
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: "Berhasil mengunggah bukti transfer",
|
||||
@@ -210,7 +202,6 @@ export default function InvestmentInvoice() {
|
||||
pickFile({
|
||||
allowedType: "image",
|
||||
setImageUri(file: any) {
|
||||
console.log("[IMAGE]", file);
|
||||
setImage(file);
|
||||
},
|
||||
});
|
||||
@@ -224,7 +215,7 @@ export default function InvestmentInvoice() {
|
||||
|
||||
<ButtonCustom
|
||||
isLoading={isLoading}
|
||||
disabled={!image}
|
||||
disabled={!image || isLoading}
|
||||
onPress={() => {
|
||||
handlerSubmitUpdate();
|
||||
}}
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCenteredOnly,
|
||||
ButtonCustom,
|
||||
InformationBox,
|
||||
LandscapeFrameUploaded,
|
||||
LoaderCustom,
|
||||
NewWrapper,
|
||||
SelectCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
TextInputCustom
|
||||
} from "@/components";
|
||||
import API_STRORAGE from "@/constants/base-url-api-strorage";
|
||||
import DIRECTORY_ID from "@/constants/directory-id";
|
||||
@@ -18,10 +19,7 @@ import {
|
||||
apiInvestmentUpdateData,
|
||||
} from "@/service/api-client/api-investment";
|
||||
import { apiMasterInvestment } from "@/service/api-client/api-master";
|
||||
import {
|
||||
deleteFileService,
|
||||
uploadFileService,
|
||||
} from "@/service/upload-service";
|
||||
import { deleteFileService, uploadFileService } from "@/service/upload-service";
|
||||
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
||||
import pickFile from "@/utils/pickFile";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
@@ -70,7 +68,7 @@ export default function InvestmentEdit() {
|
||||
useCallback(() => {
|
||||
onLoadMaster();
|
||||
onLoadData();
|
||||
}, [id])
|
||||
}, [id]),
|
||||
);
|
||||
|
||||
const onLoadMaster = async () => {
|
||||
@@ -178,7 +176,7 @@ export default function InvestmentEdit() {
|
||||
const responseUpdate = await apiInvestmentUpdateData({
|
||||
id: id as string,
|
||||
data: newData,
|
||||
category: "data"
|
||||
category: "data",
|
||||
});
|
||||
|
||||
if (responseUpdate.success) {
|
||||
@@ -201,7 +199,15 @@ export default function InvestmentEdit() {
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewWrapper>
|
||||
<NewWrapper
|
||||
footerComponent={
|
||||
<BoxButtonOnFooter>
|
||||
<ButtonCustom isLoading={isLoading} onPress={handleSubmitUpdate}>
|
||||
Simpan
|
||||
</ButtonCustom>
|
||||
</BoxButtonOnFooter>
|
||||
}
|
||||
>
|
||||
<StackCustom gap={"xs"}>
|
||||
<InformationBox text="Gambar investasi bisa berupa ilustrasi, poster atau foto terkait investasi." />
|
||||
<LandscapeFrameUploaded
|
||||
@@ -256,6 +262,8 @@ export default function InvestmentEdit() {
|
||||
/>
|
||||
|
||||
<TextInputCustom
|
||||
iconLeft="Rp."
|
||||
// disabled
|
||||
required
|
||||
placeholder="0"
|
||||
label="Total Lembar"
|
||||
@@ -341,11 +349,7 @@ export default function InvestmentEdit() {
|
||||
)}
|
||||
|
||||
<Spacing />
|
||||
<ButtonCustom isLoading={isLoading} onPress={handleSubmitUpdate}>
|
||||
Simpan
|
||||
</ButtonCustom>
|
||||
</StackCustom>
|
||||
<Spacing height={50} />
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import {
|
||||
BaseBox,
|
||||
BoxButtonOnFooter,
|
||||
ButtonCenteredOnly,
|
||||
ButtonCustom,
|
||||
CenterCustom,
|
||||
InformationBox,
|
||||
LandscapeFrameUploaded,
|
||||
LoaderCustom,
|
||||
NewWrapper,
|
||||
SelectCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import DIRECTORY_ID from "@/constants/directory-id";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
@@ -54,7 +55,7 @@ export default function InvestmentCreate() {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadMaster();
|
||||
}, [])
|
||||
}, []),
|
||||
);
|
||||
|
||||
const onLoadMaster = async () => {
|
||||
@@ -167,7 +168,7 @@ export default function InvestmentCreate() {
|
||||
text1: "Berhasil",
|
||||
text2: response.message,
|
||||
});
|
||||
router.replace("/investment/portofolio");
|
||||
router.replace("/investment/portofolio?status=review");
|
||||
} else {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
@@ -184,7 +185,19 @@ export default function InvestmentCreate() {
|
||||
|
||||
// const [coba, setCoba] = useState("");
|
||||
return (
|
||||
<ViewWrapper>
|
||||
<NewWrapper
|
||||
footerComponent={
|
||||
<BoxButtonOnFooter>
|
||||
<ButtonCustom
|
||||
disabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
onPress={() => handleSubmit()}
|
||||
>
|
||||
Simpan
|
||||
</ButtonCustom>
|
||||
</BoxButtonOnFooter>
|
||||
}
|
||||
>
|
||||
<StackCustom gap={"xs"}>
|
||||
<InformationBox text="Gambar investasi bisa berupa ilustrasi, poster atau foto terkait investasi." />
|
||||
<LandscapeFrameUploaded image={image as string} />
|
||||
@@ -224,7 +237,6 @@ export default function InvestmentCreate() {
|
||||
onPress={() => {
|
||||
pickFile({
|
||||
setPdfUri: ({ uri, name, size }) => {
|
||||
|
||||
setPdf({ uri, name, size });
|
||||
},
|
||||
allowedType: "pdf",
|
||||
@@ -265,6 +277,9 @@ export default function InvestmentCreate() {
|
||||
|
||||
<StackCustom gap={0}>
|
||||
<TextInputCustom
|
||||
iconLeft="Rp."
|
||||
// disabled
|
||||
editable={false}
|
||||
required
|
||||
placeholder="0"
|
||||
label="Total Lembar"
|
||||
@@ -291,7 +306,7 @@ export default function InvestmentCreate() {
|
||||
/>
|
||||
|
||||
{loadingMaster ? (
|
||||
<LoaderCustom />
|
||||
<CustomSkeleton height={50} />
|
||||
) : (
|
||||
<SelectCustom
|
||||
required
|
||||
@@ -313,7 +328,7 @@ export default function InvestmentCreate() {
|
||||
)}
|
||||
|
||||
{loadingMaster ? (
|
||||
<LoaderCustom />
|
||||
<CustomSkeleton height={50} />
|
||||
) : (
|
||||
<SelectCustom
|
||||
required
|
||||
@@ -335,7 +350,7 @@ export default function InvestmentCreate() {
|
||||
)}
|
||||
|
||||
{loadingMaster ? (
|
||||
<LoaderCustom />
|
||||
<CustomSkeleton height={50} />
|
||||
) : (
|
||||
<SelectCustom
|
||||
required
|
||||
@@ -357,11 +372,8 @@ export default function InvestmentCreate() {
|
||||
)}
|
||||
|
||||
<Spacing />
|
||||
<ButtonCustom isLoading={isLoading} onPress={() => handleSubmit()}>
|
||||
Simpan
|
||||
</ButtonCustom>
|
||||
</StackCustom>
|
||||
<Spacing height={50} />
|
||||
</ViewWrapper>
|
||||
{/* <Spacing height={50} /> */}
|
||||
</NewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,57 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { BaseBox, LoaderCustom, TextCustom, ViewWrapper } from "@/components";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { apiJobGetAll } from "@/service/api-client/api-job";
|
||||
import { useFocusEffect } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import Job_ScreenArchive2 from "@/screens/Job/ScreenArchive2";
|
||||
|
||||
export default function JobArchive() {
|
||||
const { user } = useAuth();
|
||||
const [listData, setListData] = useState<any[]>([]);
|
||||
const [isLoadData, setIsLoadData] = useState(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [user?.id])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
setIsLoadData(true);
|
||||
const response = await apiJobGetAll({
|
||||
category: "archive",
|
||||
authorId: user?.id,
|
||||
});
|
||||
setListData(response.data);
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setIsLoadData(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewWrapper hideFooter>
|
||||
{isLoadData ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<TextCustom align="center">Anda tidak memiliki arsip</TextCustom>
|
||||
) : (
|
||||
listData.map((item, index) => (
|
||||
<BaseBox
|
||||
key={index}
|
||||
paddingTop={20}
|
||||
paddingBottom={20}
|
||||
href={`/job/${item.id}/archive`}
|
||||
>
|
||||
<TextCustom align="center" bold truncate size="large">
|
||||
{item?.title || "-"}
|
||||
</TextCustom>
|
||||
</BaseBox>
|
||||
))
|
||||
)}
|
||||
</ViewWrapper>
|
||||
<>
|
||||
<Job_ScreenArchive2 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,83 +1,10 @@
|
||||
import {
|
||||
AvatarUsernameAndOtherComponent,
|
||||
BoxWithHeaderSection,
|
||||
FloatingButton,
|
||||
LoaderCustom,
|
||||
SearchInput,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import { apiJobGetAll } from "@/service/api-client/api-job";
|
||||
import { router, useFocusEffect } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import Job_ScreenBeranda from "@/screens/Job/ScreenBeranda";
|
||||
import Job_ScreenBeranda2 from "@/screens/Job/ScreenBeranda2";
|
||||
|
||||
export default function JobBeranda() {
|
||||
const [listData, setListData] = useState<any[]>([]);
|
||||
const [isLoadData, setIsLoadData] = useState(false);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData(search);
|
||||
}, [search])
|
||||
);
|
||||
|
||||
const onLoadData = async (search: string) => {
|
||||
try {
|
||||
setIsLoadData(true);
|
||||
const response = await apiJobGetAll({ search, category: "beranda" });
|
||||
setListData(response.data);
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setIsLoadData(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = (search: string) => {
|
||||
setSearch(search);
|
||||
onLoadData(search);
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewWrapper
|
||||
hideFooter
|
||||
floatingButton={
|
||||
<FloatingButton onPress={() => router.push("/job/create")} />
|
||||
}
|
||||
headerComponent={
|
||||
<SearchInput placeholder="Cari pekerjaan" onChangeText={handleSearch} />
|
||||
}
|
||||
>
|
||||
{isLoadData ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<TextCustom align="center">Belum ada lowongan</TextCustom>
|
||||
) : (
|
||||
listData.map((item, index) => (
|
||||
<BoxWithHeaderSection
|
||||
key={index}
|
||||
onPress={() => router.push(`/job/${item.id}`)}
|
||||
>
|
||||
<StackCustom>
|
||||
<AvatarUsernameAndOtherComponent
|
||||
avatar={item?.Author?.Profile?.imageId}
|
||||
avatarHref={`/profile/${item?.Author?.Profile?.id}`}
|
||||
name={item?.Author?.username}
|
||||
/>
|
||||
|
||||
<TextCustom truncate={2} align="center" bold size="large">
|
||||
{item?.title || "-"}
|
||||
</TextCustom>
|
||||
</StackCustom>
|
||||
<Spacing />
|
||||
</BoxWithHeaderSection>
|
||||
))
|
||||
)}
|
||||
<Spacing />
|
||||
</ViewWrapper>
|
||||
<>
|
||||
<Job_ScreenBeranda2 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,91 +1,12 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BaseBox,
|
||||
LoaderCustom,
|
||||
ScrollableCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
|
||||
import { apiJobGetByStatus } from "@/service/api-client/api-job";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import Job_MainViewStatus from "@/screens/Job/MainViewStatus";
|
||||
import Job_MainViewStatus2 from "@/screens/Job/MainViewStatus2";
|
||||
|
||||
export default function JobStatus() {
|
||||
const { user } = useAuth();
|
||||
const { status } = useLocalSearchParams<{ status?: string }>();
|
||||
console.log("STATUS", status);
|
||||
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>(
|
||||
status || "publish"
|
||||
);
|
||||
const [listData, setListData] = useState<any[]>([]);
|
||||
const [isLoadList, setIsLoadList] = useState(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [user?.id, activeCategory])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
setIsLoadList(true);
|
||||
const response = await apiJobGetByStatus({
|
||||
authorId: user?.id as string,
|
||||
status: activeCategory as string,
|
||||
});
|
||||
setListData(response.data);
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setIsLoadList(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 headerComponent={scrollComponent} hideFooter>
|
||||
{isLoadList ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<TextCustom align="center">
|
||||
Tidak ada data {activeCategory}
|
||||
</TextCustom>
|
||||
) : (
|
||||
listData.map((e, i) => (
|
||||
<BaseBox
|
||||
key={i}
|
||||
paddingTop={20}
|
||||
paddingBottom={20}
|
||||
href={`/job/${e?.id}/${activeCategory}/detail`}
|
||||
>
|
||||
<TextCustom align="center" bold truncate size="large">
|
||||
{e?.title}
|
||||
</TextCustom>
|
||||
</BaseBox>
|
||||
))
|
||||
)}
|
||||
</ViewWrapper>
|
||||
{/* <Job_MainViewStatus /> */}
|
||||
<Job_MainViewStatus2 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCenteredOnly,
|
||||
ButtonCustom,
|
||||
InformationBox,
|
||||
LandscapeFrameUploaded,
|
||||
NewWrapper,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextAreaCustom,
|
||||
TextInputCustom,
|
||||
ViewWrapper
|
||||
TextInputCustom
|
||||
} from "@/components";
|
||||
import DIRECTORY_ID from "@/constants/directory-id";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
@@ -99,16 +100,17 @@ export default function JobCreate() {
|
||||
const buttonSubmit = () => {
|
||||
return (
|
||||
<>
|
||||
<ButtonCustom isLoading={isLoading} onPress={() => handlerOnSubmit()}>
|
||||
Simpan
|
||||
</ButtonCustom>
|
||||
<Spacing />
|
||||
<BoxButtonOnFooter>
|
||||
<ButtonCustom isLoading={isLoading} onPress={() => handlerOnSubmit()}>
|
||||
Simpan
|
||||
</ButtonCustom>
|
||||
</BoxButtonOnFooter>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewWrapper>
|
||||
<NewWrapper footerComponent={buttonSubmit()}>
|
||||
<StackCustom gap={"xs"}>
|
||||
<InformationBox text="Poster atau gambar lowongan kerja bersifat opsional, tidak wajib untuk dimasukkan dan upload lah gambar yang sesuai dengan deskripsi lowongan kerja." />
|
||||
|
||||
@@ -160,9 +162,7 @@ export default function JobCreate() {
|
||||
value={data.deskripsi}
|
||||
onChangeText={(value) => setData({ ...data, deskripsi: value })}
|
||||
/>
|
||||
|
||||
{buttonSubmit()}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,248 +1,11 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
AlertDefaultSystem,
|
||||
BackButton,
|
||||
BaseBox,
|
||||
DrawerCustom,
|
||||
MenuDrawerDynamicGrid,
|
||||
NewWrapper,
|
||||
ScrollableCustom,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
} from "@/components";
|
||||
import { IconDot } from "@/components/_Icon/IconComponent";
|
||||
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
|
||||
import NoDataText from "@/components/_ShareComponent/NoDataText";
|
||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useNotificationStore } from "@/hooks/use-notification-store";
|
||||
import { apiGetNotificationsById } from "@/service/api-notifications";
|
||||
import { listOfcategoriesAppNotification } from "@/types/type-notification-category";
|
||||
import { formatChatTime } from "@/utils/formatChatTime";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router, Stack, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { RefreshControl, View } from "react-native";
|
||||
import ScreenNotification_V1 from "@/screens/Notification/ScreenNotification_V1";
|
||||
import ScreenNotification_V2 from "@/screens/Notification/ScreenNotification_V2";
|
||||
|
||||
const selectedCategory = (value: string) => {
|
||||
const category = listOfcategoriesAppNotification.find(
|
||||
(c) => c.value === value
|
||||
);
|
||||
return category?.label;
|
||||
};
|
||||
|
||||
const fixPath = ({
|
||||
deepLink,
|
||||
categoryApp,
|
||||
}: {
|
||||
deepLink: string;
|
||||
categoryApp: string;
|
||||
}) => {
|
||||
if (categoryApp === "OTHER") {
|
||||
return deepLink;
|
||||
}
|
||||
|
||||
const separator = deepLink.includes("?") ? "&" : "?";
|
||||
|
||||
const fixedPath = `${deepLink}${separator}from=notifications&category=${_.lowerCase(
|
||||
categoryApp
|
||||
)}`;
|
||||
|
||||
console.log("Fix Path", fixedPath);
|
||||
|
||||
return fixedPath;
|
||||
};
|
||||
|
||||
const BoxNotification = ({
|
||||
data,
|
||||
activeCategory,
|
||||
}: {
|
||||
data: any;
|
||||
activeCategory: string | null;
|
||||
}) => {
|
||||
// console.log("DATA NOTIFICATION", JSON.stringify(data, null, 2));
|
||||
const { markAsRead } = useNotificationStore();
|
||||
export default function Notification() {
|
||||
return (
|
||||
<>
|
||||
<BaseBox
|
||||
backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue}
|
||||
onPress={() => {
|
||||
// console.log(
|
||||
// "Notification >",
|
||||
// selectedCategory(activeCategory as string)
|
||||
// );
|
||||
const newPath = fixPath({
|
||||
deepLink: data.deepLink,
|
||||
categoryApp: data.kategoriApp,
|
||||
});
|
||||
|
||||
router.navigate(newPath as any);
|
||||
selectedCategory(activeCategory as string);
|
||||
|
||||
if (!data.isRead) {
|
||||
markAsRead(data.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<StackCustom>
|
||||
<TextCustom truncate={2} bold>
|
||||
{data.title}
|
||||
</TextCustom>
|
||||
|
||||
<TextCustom truncate={2}>{data.pesan}</TextCustom>
|
||||
|
||||
<TextCustom size="small" color="gray">
|
||||
{formatChatTime(data.createdAt)}
|
||||
</TextCustom>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default function Notifications() {
|
||||
const { user } = useAuth();
|
||||
const { category } = useLocalSearchParams<{ category?: string }>();
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>(
|
||||
category || "event"
|
||||
);
|
||||
const [listData, setListData] = useState<any[]>([]);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [openDrawer, setOpenDrawer] = useState(false);
|
||||
|
||||
const { markAsReadAll } = useNotificationStore();
|
||||
|
||||
const handlePress = (item: any) => {
|
||||
setActiveCategory(item.value);
|
||||
// tambahkan logika lain seperti filter dsb.
|
||||
};
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
fecthData();
|
||||
}, [activeCategory])
|
||||
);
|
||||
|
||||
const fecthData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await apiGetNotificationsById({
|
||||
id: user?.id as any,
|
||||
category: activeCategory as any,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setListData(response.data);
|
||||
} else {
|
||||
setListData([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error Notification", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onRefresh = () => {
|
||||
setRefreshing(true);
|
||||
fecthData();
|
||||
setRefreshing(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Notifikasi",
|
||||
headerLeft: () => <BackButton />,
|
||||
headerRight: () => (
|
||||
<IconDot
|
||||
color={MainColor.yellow}
|
||||
onPress={() => setOpenDrawer(true)}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<NewWrapper
|
||||
headerComponent={
|
||||
<ScrollableCustom
|
||||
data={listOfcategoriesAppNotification.map((e, i) => ({
|
||||
id: i,
|
||||
label: e.label,
|
||||
value: e.value,
|
||||
}))}
|
||||
onButtonPress={handlePress}
|
||||
activeId={activeCategory as string}
|
||||
/>
|
||||
}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
||||
}
|
||||
>
|
||||
{loading ? (
|
||||
<ListSkeletonComponent />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<NoDataText text="Belum ada notifikasi" />
|
||||
) : (
|
||||
listData.map((e, i) => (
|
||||
<View key={i}>
|
||||
<BoxNotification
|
||||
data={e}
|
||||
activeCategory={activeCategory as any}
|
||||
/>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</NewWrapper>
|
||||
|
||||
<DrawerCustom
|
||||
isVisible={openDrawer}
|
||||
closeDrawer={() => setOpenDrawer(false)}
|
||||
height={"auto"}
|
||||
>
|
||||
<MenuDrawerDynamicGrid
|
||||
data={[
|
||||
{
|
||||
label: "Tandai Semua Dibaca",
|
||||
value: "read-all",
|
||||
icon: (
|
||||
<Ionicons
|
||||
name="reader-outline"
|
||||
size={ICON_SIZE_SMALL}
|
||||
color={MainColor.white}
|
||||
/>
|
||||
),
|
||||
path: "",
|
||||
},
|
||||
]}
|
||||
onPressItem={(item: any) => {
|
||||
console.log("Item", item.value);
|
||||
if (item.value === "read-all") {
|
||||
AlertDefaultSystem({
|
||||
title: "Tandai Semua Dibaca",
|
||||
message:
|
||||
"Apakah Anda yakin ingin menandai semua notifikasi dibaca?",
|
||||
textLeft: "Batal",
|
||||
textRight: "Ya",
|
||||
onPressRight: () => {
|
||||
markAsReadAll(user?.id as any);
|
||||
const data = _.cloneDeep(listData);
|
||||
data.forEach((e) => {
|
||||
e.isRead = true;
|
||||
});
|
||||
setListData(data);
|
||||
onRefresh();
|
||||
setOpenDrawer(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</DrawerCustom>
|
||||
<ScreenNotification_V2 />
|
||||
{/* <ScreenNotification_V1 /> */}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
CenterCustom,
|
||||
Grid,
|
||||
InformationBox,
|
||||
NewWrapper,
|
||||
SelectCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
@@ -120,7 +121,7 @@ export default function PortofolioCreate() {
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewWrapper
|
||||
<NewWrapper
|
||||
footerComponent={
|
||||
<Portofolio_ButtonCreate
|
||||
id={id as string}
|
||||
@@ -357,8 +358,8 @@ export default function PortofolioCreate() {
|
||||
setDataMedsos({ ...dataMedsos, youtube: value })
|
||||
}
|
||||
/>
|
||||
<Spacing />
|
||||
{/* <Spacing /> */}
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,14 +4,15 @@ import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCustom,
|
||||
CenterCustom,
|
||||
NewWrapper,
|
||||
SelectCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextAreaCustom,
|
||||
TextCustom,
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
|
||||
import {
|
||||
@@ -238,7 +239,7 @@ export default function PortofolioEdit() {
|
||||
return !dataArray.some(
|
||||
(item: any) =>
|
||||
!item.MasterSubBidangBisnis.id ||
|
||||
item.MasterSubBidangBisnis.id.trim() === ""
|
||||
item.MasterSubBidangBisnis.id.trim() === "",
|
||||
);
|
||||
}
|
||||
|
||||
@@ -319,16 +320,16 @@ export default function PortofolioEdit() {
|
||||
if (!bidangBisnis || !subBidangBisnis) {
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper>
|
||||
<ActivityIndicator size="large" color={MainColor.yellow} />
|
||||
</ViewWrapper>
|
||||
<NewWrapper>
|
||||
<ListSkeletonComponent height={80} />
|
||||
</NewWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper footerComponent={buttonUpdate}>
|
||||
<NewWrapper footerComponent={buttonUpdate}>
|
||||
<StackCustom gap={"xs"}>
|
||||
<TextInputCustom
|
||||
required
|
||||
@@ -471,7 +472,7 @@ export default function PortofolioEdit() {
|
||||
/>
|
||||
<Spacing />
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,9 @@
|
||||
import { TextCustom, ViewWrapper } from "@/components";
|
||||
import Portofolio_BoxView from "@/screens/Portofolio/BoxPortofolioView";
|
||||
import { apiGetPortofolio } from "@/service/api-client/api-portofolio";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
import ViewListPortofolio from "@/screens/Portofolio/ViewListPortofolio";
|
||||
|
||||
export default function ListPortofolio() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadPortofolio(id as string);
|
||||
}, [id])
|
||||
);
|
||||
|
||||
const onLoadPortofolio = async (id: string) => {
|
||||
const response = await apiGetPortofolio({ id: id });
|
||||
setData(response.data);
|
||||
};
|
||||
return (
|
||||
<ViewWrapper>
|
||||
{data ? data?.map((item: any, index: number) => (
|
||||
<Portofolio_BoxView key={index} data={item} />
|
||||
)) : <TextCustom>Tidak ada portofolio</TextCustom>}
|
||||
</ViewWrapper>
|
||||
<>
|
||||
<ViewListPortofolio />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export default function ProfileLayout() {
|
||||
|
||||
<Stack.Screen
|
||||
name="[id]/blocked-list"
|
||||
options={{ title: "Blocked List", headerLeft: () => <BackButton /> }}
|
||||
options={{ title: "Daftar Blokir", headerLeft: () => <BackButton /> }}
|
||||
/>
|
||||
|
||||
<Stack.Screen
|
||||
|
||||
@@ -1,115 +1,11 @@
|
||||
import {
|
||||
AvatarComp,
|
||||
ClickableCustom,
|
||||
Grid,
|
||||
LoaderCustom,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
||||
import { apiAllUser } from "@/service/api-client/api-user";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useEffect, useState } from "react";
|
||||
import UserSearchMainView from "@/screens/UserSeach/MainView";
|
||||
import UserSearchMainView_V2 from "@/screens/UserSeach/MainView_V2";
|
||||
|
||||
export default function UserSearch() {
|
||||
const [data, setData] = useState<any[]>([]);
|
||||
const [search, setSearch] = useState<string>("");
|
||||
const [isLoadList, setIsLoadList] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
onLoadData(search);
|
||||
}, [search]);
|
||||
|
||||
const onLoadData = async (search: string) => {
|
||||
try {
|
||||
setIsLoadList(true);
|
||||
const response = await apiAllUser({ search: search });
|
||||
console.log("[DATA USER] >", JSON.stringify(response.data, null, 2));
|
||||
setData(response.data);
|
||||
} catch (error) {
|
||||
console.log("Error fetching data", error);
|
||||
} finally {
|
||||
setIsLoadList(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSearch = (search: string) => {
|
||||
setSearch(search);
|
||||
onLoadData(search);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper
|
||||
headerComponent={
|
||||
<TextInputCustom
|
||||
value={search}
|
||||
onChangeText={handleSearch}
|
||||
iconLeft={
|
||||
<Ionicons
|
||||
name="search"
|
||||
size={ICON_SIZE_SMALL}
|
||||
color={MainColor.placeholder}
|
||||
/>
|
||||
}
|
||||
placeholder="Cari Pengguna"
|
||||
borderRadius={50}
|
||||
containerStyle={{ marginBottom: 0 }}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<StackCustom>
|
||||
{isLoadList ? (
|
||||
<LoaderCustom />
|
||||
) : !_.isEmpty(data) ? (
|
||||
data?.map((e, index) => {
|
||||
return (
|
||||
<ClickableCustom
|
||||
key={index}
|
||||
onPress={() => {
|
||||
console.log("Ke Profile");
|
||||
router.push(`/profile/${e?.Profile?.id}`);
|
||||
}}
|
||||
>
|
||||
<Grid>
|
||||
<Grid.Col span={2}>
|
||||
<AvatarComp fileId={e?.Profile?.imageId} size="base" />
|
||||
</Grid.Col>
|
||||
<Grid.Col span={9}>
|
||||
<StackCustom gap={"sm"}>
|
||||
<TextCustom size="large">{e?.username}</TextCustom>
|
||||
<TextCustom size="small">+{e?.nomor}</TextCustom>
|
||||
</StackCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col
|
||||
span={1}
|
||||
style={{
|
||||
justifyContent: "center",
|
||||
alignItems: "flex-end",
|
||||
}}
|
||||
>
|
||||
<Ionicons
|
||||
name="chevron-forward"
|
||||
size={ICON_SIZE_SMALL}
|
||||
color={MainColor.white}
|
||||
/>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
</ClickableCustom>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<TextCustom align="center">Tidak ditemukan</TextCustom>
|
||||
)}
|
||||
</StackCustom>
|
||||
<Spacing height={50} />
|
||||
</ViewWrapper>
|
||||
{/* <UserSearchMainView /> */}
|
||||
<UserSearchMainView_V2 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,59 +1,6 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
LoaderCustom,
|
||||
TextCustom,
|
||||
ViewWrapper
|
||||
} from "@/components";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
|
||||
import { apiVotingGetAll } from "@/service/api-client/api-voting";
|
||||
import { useFocusEffect } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useState, useCallback } from "react";
|
||||
import Voting_ScreenContribution from "@/screens/Voting/ScreenContribution";
|
||||
|
||||
export default function VotingContribution() {
|
||||
const { user } = useAuth();
|
||||
const [listData, setListData] = useState<any>([]);
|
||||
const [loadingGetData, setLoadingGetData] = useState(false);
|
||||
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
setLoadingGetData(true);
|
||||
const response = await apiVotingGetAll({
|
||||
category: "contribution",
|
||||
authorId: user?.id as string,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setListData(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setLoadingGetData(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewWrapper hideFooter>
|
||||
{loadingGetData ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<TextCustom align="center">Tidak ada kontribusi</TextCustom>
|
||||
) : listData.map((item: any, index: number) => (
|
||||
<Voting_BoxPublishSection
|
||||
data={item}
|
||||
key={index}
|
||||
href={`/voting/${item.id}/contribution`}
|
||||
/>
|
||||
))}
|
||||
</ViewWrapper>
|
||||
);
|
||||
return <Voting_ScreenContribution />;
|
||||
}
|
||||
|
||||
@@ -1,77 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { LoaderCustom, TextCustom, ViewWrapper } from "@/components";
|
||||
import TabsTwoButtonCustom from "@/components/_ShareComponent/TabsTwoHeaderCustom";
|
||||
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useCallback, useState } from "react";
|
||||
import { apiVotingGetAll } from "@/service/api-client/api-voting";
|
||||
import { useFocusEffect } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import Voting_ScreenHistory from "@/screens/Voting/ScreenHistory";
|
||||
|
||||
export default function VotingHistory() {
|
||||
const { user } = useAuth();
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>("all");
|
||||
|
||||
const [listData, setListData] = useState<any>([]);
|
||||
const [loadingGetData, setLoadingGetData] = useState(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [activeCategory])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
setLoadingGetData(true);
|
||||
const response = await apiVotingGetAll({
|
||||
category: activeCategory === "all" ? "all-history" : "my-history",
|
||||
authorId: user?.id as string,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setListData(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setLoadingGetData(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePress = (item: any) => {
|
||||
setActiveCategory(item);
|
||||
// tambahkan logika lain seperti filter dsb.
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewWrapper
|
||||
hideFooter
|
||||
headerComponent={
|
||||
<TabsTwoButtonCustom
|
||||
leftValue="all"
|
||||
rightValue="main"
|
||||
leftText="Semua Riwayat"
|
||||
rightText="Riwayat Saya"
|
||||
activeCategory={activeCategory}
|
||||
handlePress={handlePress}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{loadingGetData ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<TextCustom align="center">Tidak ada riwayat</TextCustom>
|
||||
) : (
|
||||
listData.map((item: any, index: number) => (
|
||||
<Voting_BoxPublishSection
|
||||
key={index}
|
||||
id={item.id}
|
||||
data={item}
|
||||
href={`/voting/${item.id}/history`}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</ViewWrapper>
|
||||
<>
|
||||
<Voting_ScreenHistory />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,71 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
FloatingButton,
|
||||
LoaderCustom,
|
||||
SearchInput,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
|
||||
import { apiVotingGetAll } from "@/service/api-client/api-voting";
|
||||
import { router, useFocusEffect } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import Voting_ScreenBeranda from "@/screens/Voting/ScreenBeranda";
|
||||
|
||||
export default function VotingBeranda() {
|
||||
const { user } = useAuth();
|
||||
const [listData, setListData] = useState<any>([]);
|
||||
const [loadingGetData, setLoadingGetData] = useState(false);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [search])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
setLoadingGetData(true);
|
||||
const response = await apiVotingGetAll({
|
||||
search,
|
||||
category: "beranda",
|
||||
userLoginId: user?.id,
|
||||
});
|
||||
if (response.success) {
|
||||
setListData(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setLoadingGetData(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewWrapper
|
||||
hideFooter
|
||||
floatingButton={
|
||||
<FloatingButton onPress={() => router.push("/voting/create")} />
|
||||
}
|
||||
headerComponent={
|
||||
<SearchInput placeholder="Cari voting" onChangeText={setSearch} />
|
||||
}
|
||||
>
|
||||
{loadingGetData ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<TextCustom align="center">Tidak ada data</TextCustom>
|
||||
) : (
|
||||
listData.map((item: any, index: number) => (
|
||||
<Voting_BoxPublishSection
|
||||
data={item}
|
||||
key={index}
|
||||
href={`/voting/${item.id}`}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</ViewWrapper>
|
||||
<>
|
||||
<Voting_ScreenBeranda />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,106 +1,10 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BadgeCustom,
|
||||
BaseBox,
|
||||
LoaderCustom,
|
||||
ScrollableCustom,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
|
||||
import { apiVotingGetByStatus } from "@/service/api-client/api-voting";
|
||||
import { dateTimeView } from "@/utils/dateTimeView";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import Voting_ScreenStatus from "@/screens/Voting/ScreenStatus";
|
||||
|
||||
export default function VotingStatus() {
|
||||
const { user } = useAuth();
|
||||
const { status } = useLocalSearchParams<{ status?: string }>();
|
||||
|
||||
const id = user?.id || "";
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>(
|
||||
status || "publish"
|
||||
);
|
||||
|
||||
const [listData, setListData] = useState([]);
|
||||
const [loadingGetData, setLoadingGetData] = useState(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [activeCategory, id])
|
||||
);
|
||||
|
||||
async function onLoadData() {
|
||||
try {
|
||||
setLoadingGetData(true);
|
||||
const response = await apiVotingGetByStatus({
|
||||
id: id as string,
|
||||
status: activeCategory!,
|
||||
});
|
||||
setListData(response.data);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
} finally {
|
||||
setLoadingGetData(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 headerComponent={scrollComponent} hideFooter>
|
||||
{loadingGetData ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<TextCustom align="center">Tidak ada data {activeCategory}</TextCustom>
|
||||
) : (
|
||||
listData.map((item: any, i: number) => (
|
||||
<BaseBox
|
||||
key={i}
|
||||
paddingTop={20}
|
||||
paddingBottom={20}
|
||||
href={`/voting/${item.id}/${activeCategory}/detail`}
|
||||
>
|
||||
<StackCustom>
|
||||
<TextCustom align="center" bold truncate={2} size="large">
|
||||
{item?.title || ""}
|
||||
</TextCustom>
|
||||
<BadgeCustom
|
||||
style={{ width: "70%", alignSelf: "center" }}
|
||||
variant="light"
|
||||
>
|
||||
{item?.awalVote &&
|
||||
dateTimeView({
|
||||
date: item?.awalVote,
|
||||
withoutTime: true,
|
||||
})}{" "}
|
||||
-{" "}
|
||||
{item?.akhirVote &&
|
||||
dateTimeView({ date: item?.akhirVote, withoutTime: true })}
|
||||
</BadgeCustom>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
))
|
||||
)}
|
||||
</ViewWrapper>
|
||||
<>
|
||||
<Voting_ScreenStatus />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,8 +51,6 @@ export default function VotingDetailStatus() {
|
||||
setLoadingGetData(true);
|
||||
const response = await apiVotingGetOne({ id: id as string });
|
||||
|
||||
console.log("[DATA BY ID]", JSON.stringify(response, null, 2));
|
||||
|
||||
if (response.success) {
|
||||
setData(response.data);
|
||||
}
|
||||
|
||||
@@ -5,13 +5,14 @@ import {
|
||||
ButtonCustom,
|
||||
CenterCustom,
|
||||
LoaderCustom,
|
||||
NewWrapper,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextAreaCustom,
|
||||
TextCustom,
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
TextInputCustom
|
||||
} from "@/components";
|
||||
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
|
||||
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
|
||||
@@ -34,7 +35,7 @@ interface IEditData {
|
||||
Voting_DaftarNamaVote?: [
|
||||
{
|
||||
value?: string;
|
||||
}
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -47,7 +48,7 @@ export default function VotingEdit() {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id])
|
||||
}, [id]),
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
@@ -188,9 +189,9 @@ export default function VotingEdit() {
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewWrapper footerComponent={buttonSubmit()}>
|
||||
<NewWrapper footerComponent={buttonSubmit()}>
|
||||
{loadingGetData ? (
|
||||
<LoaderCustom />
|
||||
<ListSkeletonComponent />
|
||||
) : (
|
||||
<StackCustom gap={"xs"}>
|
||||
<TextInputCustom
|
||||
@@ -210,7 +211,7 @@ export default function VotingEdit() {
|
||||
onChangeText={(text) => setData({ ...data, deskripsi: text })}
|
||||
/>
|
||||
|
||||
<Spacing />
|
||||
|
||||
|
||||
<DateTimePickerCustom
|
||||
minimumDate={new Date(Date.now())}
|
||||
@@ -255,7 +256,7 @@ export default function VotingEdit() {
|
||||
}
|
||||
</TextCustom>
|
||||
)}
|
||||
<Spacing />
|
||||
|
||||
</StackCustom>
|
||||
|
||||
{data?.Voting_DaftarNamaVote?.map((item: any, index: number) => (
|
||||
@@ -270,7 +271,7 @@ export default function VotingEdit() {
|
||||
...(data as any),
|
||||
Voting_DaftarNamaVote: data?.Voting_DaftarNamaVote?.map(
|
||||
(item: any, i: any) =>
|
||||
i === index ? { ...item, value } : item
|
||||
i === index ? { ...item, value } : item,
|
||||
),
|
||||
})
|
||||
}
|
||||
@@ -327,6 +328,6 @@ export default function VotingEdit() {
|
||||
<Spacing />
|
||||
</StackCustom>
|
||||
)}
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,69 +1,6 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
AvatarUsernameAndOtherComponent,
|
||||
BadgeCustom,
|
||||
BaseBox,
|
||||
LoaderCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import { apiVotingContribution } from "@/service/api-client/api-voting";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import Voting_ScreenListOfContributor from "@/screens/Voting/ScreenListOfContributor";
|
||||
|
||||
export default function Voting_ListOfContributor() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const [listData, setListData] = useState<any>([]);
|
||||
const [isLoadData, setIsLoadData] = useState(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadList();
|
||||
}, [id])
|
||||
);
|
||||
|
||||
const onLoadList = async () => {
|
||||
try {
|
||||
setIsLoadData(true);
|
||||
const response = await apiVotingContribution({
|
||||
id: id as string,
|
||||
authorId: "",
|
||||
category: "list",
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setListData(response.data);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setIsLoadData(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewWrapper>
|
||||
{isLoadData ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<TextCustom align="center">Tidak ada kontributor</TextCustom>
|
||||
) : (
|
||||
listData.map((item: any, index: number) => (
|
||||
<BaseBox paddingTop={5} paddingBottom={5} key={index.toString()}>
|
||||
<AvatarUsernameAndOtherComponent
|
||||
avatar={item?.Author?.Profile?.imageId || ""}
|
||||
name={item?.Author?.username || "Username"}
|
||||
avatarHref={`/profile/${item?.Author?.Profile?.id}`}
|
||||
rightComponent={
|
||||
<BadgeCustom style={{ alignSelf: "flex-end" }}>
|
||||
{item?.Voting_DaftarNamaVote?.value}
|
||||
</BadgeCustom>
|
||||
}
|
||||
/>
|
||||
</BaseBox>
|
||||
))
|
||||
)}
|
||||
</ViewWrapper>
|
||||
);
|
||||
export default function VotingListOfContributor() {
|
||||
return <Voting_ScreenListOfContributor />;
|
||||
}
|
||||
|
||||
@@ -3,11 +3,12 @@ import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCustom,
|
||||
CenterCustom,
|
||||
NewWrapper,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextAreaCustom,
|
||||
TextInputCustom,
|
||||
ViewWrapper
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
@@ -79,7 +80,7 @@ export default function VotingCreate() {
|
||||
type: "success",
|
||||
text1: "Data berhasil disimpan",
|
||||
});
|
||||
router.replace("/(application)/(user)/voting/(tabs)/status?status=review");
|
||||
router.replace("/voting/(tabs)/status?status=review");
|
||||
} else {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
@@ -106,7 +107,7 @@ export default function VotingCreate() {
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewWrapper footerComponent={buttonSubmit()}>
|
||||
<NewWrapper footerComponent={buttonSubmit()}>
|
||||
<StackCustom gap={"xs"}>
|
||||
<TextInputCustom
|
||||
label="Judul Voting"
|
||||
@@ -142,7 +143,6 @@ export default function VotingCreate() {
|
||||
}
|
||||
/>
|
||||
|
||||
|
||||
{listVote.map((item, index) => (
|
||||
<TextInputCustom
|
||||
key={index}
|
||||
@@ -153,8 +153,8 @@ export default function VotingCreate() {
|
||||
onChangeText={(value: any) =>
|
||||
setListVote(
|
||||
listVote.map((item, i) =>
|
||||
i === index ? { ...item, value } : item
|
||||
)
|
||||
i === index ? { ...item, value } : item,
|
||||
),
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -198,6 +198,6 @@ export default function VotingCreate() {
|
||||
|
||||
<Spacing />
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
</NewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
AlertDefaultSystem,
|
||||
BadgeCustom,
|
||||
BaseBox,
|
||||
BoxButtonOnFooter,
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
} from "@/components";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import {
|
||||
apiAdminDonationInvoiceDetailById,
|
||||
apiAdminDonationInvoiceUpdateById,
|
||||
@@ -22,6 +24,7 @@ import { useCallback, useState } from "react";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export default function AdminDonasiTransactionDetail() {
|
||||
const { user } = useAuth();
|
||||
const { id, status } = useLocalSearchParams();
|
||||
console.log("[STATUS]", id, status);
|
||||
|
||||
@@ -33,7 +36,7 @@ export default function AdminDonasiTransactionDetail() {
|
||||
onLoadData();
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [id])
|
||||
}, [id]),
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
@@ -57,6 +60,7 @@ export default function AdminDonasiTransactionDetail() {
|
||||
const newData = {
|
||||
donationId: data?.donasiId,
|
||||
nominal: data?.nominal,
|
||||
senderId: user?.id,
|
||||
};
|
||||
|
||||
const response = await apiAdminDonationInvoiceUpdateById({
|
||||
@@ -97,7 +101,15 @@ export default function AdminDonasiTransactionDetail() {
|
||||
<ButtonCustom
|
||||
isLoading={isLoading}
|
||||
onPress={() => {
|
||||
handlerSubmit();
|
||||
AlertDefaultSystem({
|
||||
title: "Konfirmasi transaksi",
|
||||
message: "Apakah anda yakin ingin menyetujui transaksi ini?",
|
||||
textLeft: "Tidak",
|
||||
textRight: "Ya",
|
||||
onPressRight: () => {
|
||||
handlerSubmit();
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Terima donasi
|
||||
@@ -109,7 +121,7 @@ export default function AdminDonasiTransactionDetail() {
|
||||
return (
|
||||
<BoxButtonOnFooter>
|
||||
<ButtonCustom disabled>
|
||||
{data?.DonasiMaster_StatusInvoice?.name}
|
||||
{data?.DonasiMaster_StatusInvoice?.name}
|
||||
</ButtonCustom>
|
||||
</BoxButtonOnFooter>
|
||||
);
|
||||
@@ -140,7 +152,7 @@ export default function AdminDonasiTransactionDetail() {
|
||||
})}
|
||||
>
|
||||
{_.startCase(
|
||||
(data?.DonasiMaster_StatusInvoice?.name as any) || "-"
|
||||
(data?.DonasiMaster_StatusInvoice?.name as any) || "-",
|
||||
)}
|
||||
</BadgeCustom>
|
||||
)) ||
|
||||
@@ -157,7 +169,7 @@ export default function AdminDonasiTransactionDetail() {
|
||||
<ButtonCustom
|
||||
onPress={() =>
|
||||
router.push(
|
||||
`/(application)/(image)/preview-image/${data?.imageId}`
|
||||
`/(application)/(image)/preview-image/${data?.imageId}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
|
||||
@@ -14,7 +14,11 @@ import {
|
||||
} from "@/components";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import DIRECTORY_ID from "@/constants/directory-id";
|
||||
import { apiAdminDonationDetailById, apiAdminDonationDisbursementOfFundsCreated } from "@/service/api-admin/api-admin-donation";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import {
|
||||
apiAdminDonationDetailById,
|
||||
apiAdminDonationDisbursementOfFundsCreated,
|
||||
} from "@/service/api-admin/api-admin-donation";
|
||||
import { uploadFileService } from "@/service/upload-service";
|
||||
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
||||
import pickFile from "@/utils/pickFile";
|
||||
@@ -25,7 +29,7 @@ import Toast from "react-native-toast-message";
|
||||
|
||||
export default function AdminDonationDisbursementOfFunds() {
|
||||
const { id } = useLocalSearchParams();
|
||||
|
||||
const { user } = useAuth();
|
||||
const [data, setData] = React.useState<any | null>(null);
|
||||
const [isLoading, setIsLoading] = React.useState(false);
|
||||
|
||||
@@ -40,7 +44,7 @@ export default function AdminDonationDisbursementOfFunds() {
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id])
|
||||
}, [id]),
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
@@ -94,6 +98,7 @@ export default function AdminDonationDisbursementOfFunds() {
|
||||
|
||||
const newData = {
|
||||
...value,
|
||||
authorId: user?.id,
|
||||
imageId: imageId,
|
||||
};
|
||||
|
||||
|
||||
@@ -7,15 +7,15 @@ import {
|
||||
} from "@/components";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { funUpdateStatusDonation } from "@/screens/Admin/Donation/funDonationUpdateStatus";
|
||||
import {
|
||||
apiAdminDonationDetailById
|
||||
} from "@/service/api-admin/api-admin-donation";
|
||||
import { apiAdminDonationDetailById } from "@/service/api-admin/api-admin-donation";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import React from "react";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export default function AdminDonationRejectInput() {
|
||||
const { user } = useAuth();
|
||||
const { id, status } = useLocalSearchParams();
|
||||
|
||||
const [data, setData] = React.useState<any | null>(null);
|
||||
@@ -24,7 +24,7 @@ export default function AdminDonationRejectInput() {
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id])
|
||||
}, [id]),
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
@@ -48,11 +48,23 @@ export default function AdminDonationRejectInput() {
|
||||
changeStatus: "publish" | "review" | "reject";
|
||||
}) => {
|
||||
try {
|
||||
if (!user?.id) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "User tidak ditemukan",
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
const response = await funUpdateStatusDonation({
|
||||
id: id as string,
|
||||
changeStatus,
|
||||
data: data,
|
||||
data: {
|
||||
senderId: user?.id as string,
|
||||
catatan: data,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
@@ -61,7 +73,7 @@ export default function AdminDonationRejectInput() {
|
||||
text1: "Report gagal",
|
||||
});
|
||||
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
Toast.show({
|
||||
|
||||
@@ -15,12 +15,11 @@ import { IconDot, IconView } from "@/components/_Icon/IconComponent";
|
||||
import { IconTrash } from "@/components/_Icon/IconTrash";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
||||
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
|
||||
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
||||
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import {
|
||||
apiAdminForumCommentById,
|
||||
apiAdminForumDeactivateComment,
|
||||
@@ -35,6 +34,7 @@ import Toast from "react-native-toast-message";
|
||||
|
||||
export default function AdminForumReportComment() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const { user } = useAuth();
|
||||
const [data, setData] = useState<any | null>(null);
|
||||
const [listReport, setListReport] = useState<any[] | null>(null);
|
||||
const [loadList, setLoadList] = useState(false);
|
||||
@@ -111,9 +111,13 @@ export default function AdminForumReportComment() {
|
||||
|
||||
<AdminComp_BoxTitle title="Daftar Report Komentar" />
|
||||
|
||||
<StackCustom gap={"sm"}>
|
||||
<StackCustom gap={"sm"}>
|
||||
<GridSpan_NewComponent
|
||||
text1={<TextCustom bold align="center">Aksi</TextCustom>}
|
||||
text1={
|
||||
<TextCustom bold align="center">
|
||||
Aksi
|
||||
</TextCustom>
|
||||
}
|
||||
text2={<TextCustom bold>Pelapor</TextCustom>}
|
||||
text3={<TextCustom bold>Kategori Report</TextCustom>}
|
||||
/>
|
||||
@@ -129,22 +133,24 @@ export default function AdminForumReportComment() {
|
||||
<View key={index}>
|
||||
<GridSpan_NewComponent
|
||||
text1={
|
||||
<CenterCustom>
|
||||
<ActionIcon
|
||||
icon={<IconView size={ICON_SIZE_BUTTON} color="black" />}
|
||||
onPress={() => {
|
||||
setOpenDrawerAction(true);
|
||||
setSelectedReport({
|
||||
id: item.id,
|
||||
username: item.User?.username,
|
||||
kategori: item.ForumMaster_KategoriReport?.title,
|
||||
keterangan:
|
||||
item.ForumMaster_KategoriReport?.deskripsi,
|
||||
deskripsi: item.deskripsi,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</CenterCustom>
|
||||
<CenterCustom>
|
||||
<ActionIcon
|
||||
icon={
|
||||
<IconView size={ICON_SIZE_BUTTON} color="black" />
|
||||
}
|
||||
onPress={() => {
|
||||
setOpenDrawerAction(true);
|
||||
setSelectedReport({
|
||||
id: item.id,
|
||||
username: item.User?.username,
|
||||
kategori: item.ForumMaster_KategoriReport?.title,
|
||||
keterangan:
|
||||
item.ForumMaster_KategoriReport?.deskripsi,
|
||||
deskripsi: item.deskripsi,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</CenterCustom>
|
||||
}
|
||||
text2={
|
||||
<TextCustom truncate={1}>
|
||||
@@ -188,15 +194,18 @@ export default function AdminForumReportComment() {
|
||||
onPressRight: async () => {
|
||||
const deleteComment = await apiAdminForumDeactivateComment({
|
||||
id: id as string,
|
||||
data: {
|
||||
senderId: user?.id as string,
|
||||
},
|
||||
});
|
||||
|
||||
if (!deleteComment.success) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Komentar gagal dihapus",
|
||||
});
|
||||
return;
|
||||
}
|
||||
// if (!deleteComment.success) {
|
||||
// Toast.show({
|
||||
// type: "error",
|
||||
// text1: "Komentar gagal dihapus",
|
||||
// });
|
||||
// return;
|
||||
// }
|
||||
|
||||
setOpenDrawer(false);
|
||||
Toast.show({
|
||||
|
||||
@@ -16,12 +16,11 @@ import { IconDot, IconView } from "@/components/_Icon/IconComponent";
|
||||
import { IconTrash } from "@/components/_Icon/IconTrash";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
|
||||
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
|
||||
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
|
||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
||||
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import {
|
||||
apiAdminForumDeactivatePosting,
|
||||
apiAdminForumListReportPostingById,
|
||||
@@ -35,6 +34,7 @@ import { Divider } from "react-native-paper";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export default function AdminForumReportPosting() {
|
||||
const { user } = useAuth();
|
||||
const { id } = useLocalSearchParams();
|
||||
const [openDrawerPage, setOpenDrawerPage] = useState(false);
|
||||
const [openDrawerAction, setOpenDrawerAction] = useState(false);
|
||||
@@ -215,6 +215,9 @@ export default function AdminForumReportPosting() {
|
||||
onPressRight: async () => {
|
||||
const response = await apiAdminForumDeactivatePosting({
|
||||
id: id as string,
|
||||
data: {
|
||||
senderId: user?.id as string,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.success) {
|
||||
|
||||
@@ -73,7 +73,7 @@ export default function AdminForumReportPosting() {
|
||||
<GridSpan_NewComponent
|
||||
text1={
|
||||
<TextCustom bold truncate>
|
||||
Username
|
||||
Pelapor
|
||||
</TextCustom>
|
||||
}
|
||||
text2={
|
||||
|
||||
@@ -20,6 +20,7 @@ import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButt
|
||||
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
|
||||
import AdminButtonReview from "@/components/_ShareComponent/Admin/ButtonReview";
|
||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||
import ReportBox from "@/components/Box/ReportBox";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
|
||||
@@ -28,6 +29,7 @@ import {
|
||||
apiAdminInvestmentDetailById,
|
||||
} from "@/service/api-admin/api-admin-investment";
|
||||
import { colorBadgeStatus } from "@/utils/colorBadge";
|
||||
import { countDownAndCondition } from "@/utils/countDownAndCondition";
|
||||
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
@@ -40,91 +42,41 @@ export default function AdminInvestmentDetail() {
|
||||
|
||||
const [data, setData] = React.useState<any | null>(null);
|
||||
const [isLoading, setLoading] = React.useState(false);
|
||||
const [remind, setRemind] = React.useState({
|
||||
sisa: 0,
|
||||
reminder: false,
|
||||
});
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id])
|
||||
}, [id]),
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
const response = await apiAdminInvestmentDetailById({ id: id as string });
|
||||
// console.log("[GETONE INVEST]", JSON.stringify(response, null, 2));
|
||||
if (response.success) {
|
||||
setData(response.data);
|
||||
|
||||
const duration = response?.data?.MasterPencarianInvestor?.name;
|
||||
const publishTime = response?.data?.countDown;
|
||||
|
||||
const countDown = countDownAndCondition({
|
||||
duration: duration,
|
||||
publishTime: publishTime
|
||||
});
|
||||
|
||||
setRemind({
|
||||
sisa: countDown.durationDay,
|
||||
reminder: countDown.reminder,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
console.log("Error", error);
|
||||
}
|
||||
};
|
||||
|
||||
const listData = [
|
||||
{
|
||||
label: "Username",
|
||||
value: (data && data?.author?.username) || "-",
|
||||
},
|
||||
{
|
||||
label: "Judul",
|
||||
value: (data && data?.title) || "-",
|
||||
},
|
||||
{
|
||||
label: "Status",
|
||||
value:
|
||||
data && data?.MasterStatusInvestasi?.name ? (
|
||||
<BadgeCustom
|
||||
color={colorBadgeStatus({
|
||||
status: data?.MasterStatusInvestasi?.name as string,
|
||||
})}
|
||||
>
|
||||
{_.startCase(data?.MasterStatusInvestasi?.name as string)}
|
||||
</BadgeCustom>
|
||||
) : (
|
||||
"-"
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Dana Dibutuhkan",
|
||||
value: `Rp. ${
|
||||
(data && data?.targetDana && formatCurrencyDisplay(data?.targetDana)) ||
|
||||
"-"
|
||||
}`,
|
||||
},
|
||||
{
|
||||
label: "Harga Perlembar",
|
||||
value: `Rp. ${
|
||||
(data &&
|
||||
data?.hargaLembar &&
|
||||
formatCurrencyDisplay(data?.hargaLembar)) ||
|
||||
"-"
|
||||
}`,
|
||||
},
|
||||
{
|
||||
label: "Total Lembar",
|
||||
value:
|
||||
(data &&
|
||||
data?.totalLembar &&
|
||||
formatCurrencyDisplay(data?.totalLembar)) ||
|
||||
"-",
|
||||
},
|
||||
{
|
||||
label: "ROI",
|
||||
value: `${(data && data?.roi && data?.roi) || 0} %`,
|
||||
},
|
||||
{
|
||||
label: "Pembagian Deviden",
|
||||
value: (data && data?.MasterPembagianDeviden?.name) + " bulan" || "-",
|
||||
},
|
||||
{
|
||||
label: "Jadwal Pembagian",
|
||||
value: (data && data?.MasterPeriodeDeviden?.name) || "-",
|
||||
},
|
||||
{
|
||||
label: "Pencarian Investor",
|
||||
value: (data && data?.MasterPencarianInvestor?.name) + " hari" || "-",
|
||||
},
|
||||
];
|
||||
|
||||
const handlerSubmitPublish = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
@@ -134,7 +86,6 @@ export default function AdminInvestmentDetail() {
|
||||
data: data,
|
||||
});
|
||||
|
||||
// console.log("[GET ON INVEST]", JSON.stringify(response, null, 2));
|
||||
if (!response.success) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
@@ -164,6 +115,16 @@ export default function AdminInvestmentDetail() {
|
||||
/>
|
||||
);
|
||||
|
||||
if (!data) {
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper>
|
||||
<CustomSkeleton height={200} />
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ViewWrapper
|
||||
@@ -177,8 +138,8 @@ export default function AdminInvestmentDetail() {
|
||||
{status === "publish" && (
|
||||
<BaseBox>
|
||||
<ProgressCustom
|
||||
label={data && `${data.progress}%` || "0%"}
|
||||
value={data && data.progress || 0}
|
||||
label={(data && `${data.progress}%`) || "0%"}
|
||||
value={(data && data.progress) || 0}
|
||||
size="lg"
|
||||
/>
|
||||
<Spacing />
|
||||
@@ -187,7 +148,8 @@ export default function AdminInvestmentDetail() {
|
||||
label={<TextCustom bold>Sisa Saham</TextCustom>}
|
||||
value={
|
||||
<TextCustom>
|
||||
{data && formatCurrencyDisplay(data && data?.sisaLembar)} lembar
|
||||
{data && formatCurrencyDisplay(data && data?.sisaLembar)}{" "}
|
||||
lembar
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
@@ -206,13 +168,15 @@ export default function AdminInvestmentDetail() {
|
||||
<BaseBox>
|
||||
<StackCustom>
|
||||
<DummyLandscapeImage imageId={data?.imageId} />
|
||||
{listData.map((item, i) => (
|
||||
<GridSpan_4_8
|
||||
key={i}
|
||||
label={<TextCustom bold>{item.label}</TextCustom>}
|
||||
value={<TextCustom>{item.value}</TextCustom>}
|
||||
/>
|
||||
))}
|
||||
{listData({ data: data, reminder: remind.reminder })?.map(
|
||||
(item, i) => (
|
||||
<GridSpan_4_8
|
||||
key={i}
|
||||
label={<TextCustom bold>{item.label}</TextCustom>}
|
||||
value={<TextCustom>{item.value}</TextCustom>}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
|
||||
@@ -230,7 +194,7 @@ export default function AdminInvestmentDetail() {
|
||||
}
|
||||
onPress={() => {
|
||||
router.push(
|
||||
`/(application)/(file)/${data?.prospektusFileId}`
|
||||
`/(application)/(file)/${data?.prospektusFileId}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
@@ -259,7 +223,7 @@ export default function AdminInvestmentDetail() {
|
||||
}
|
||||
onPress={() => {
|
||||
router.push(
|
||||
`/(application)/(file)/${item?.fileId}`
|
||||
`/(application)/(file)/${item?.fileId}`,
|
||||
);
|
||||
}}
|
||||
>
|
||||
@@ -299,8 +263,8 @@ export default function AdminInvestmentDetail() {
|
||||
onReject={() => {
|
||||
router.push(
|
||||
`/admin/investment/${id}/reject-input?status=${_.lowerCase(
|
||||
data?.MasterStatusInvestasi?.name
|
||||
)}`
|
||||
data?.MasterStatusInvestasi?.name,
|
||||
)}`,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
@@ -343,3 +307,67 @@ export default function AdminInvestmentDetail() {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const listData = ({ data, reminder }: { data: any; reminder: boolean }) => [
|
||||
{
|
||||
label: "Username",
|
||||
value: (data && data?.author?.username) || "-",
|
||||
},
|
||||
{
|
||||
label: "Judul",
|
||||
value: (data && data?.title) || "-",
|
||||
},
|
||||
{
|
||||
label: "Status",
|
||||
value:
|
||||
data && data?.MasterStatusInvestasi?.name ? (
|
||||
<BadgeCustom
|
||||
color={colorBadgeStatus({
|
||||
status: reminder ? "periode berakhir" : "publish",
|
||||
})}
|
||||
>
|
||||
{reminder
|
||||
? "Periode Berakhir"
|
||||
: _.startCase(data?.MasterStatusInvestasi?.name as string)}
|
||||
</BadgeCustom>
|
||||
) : (
|
||||
"-"
|
||||
),
|
||||
},
|
||||
{
|
||||
label: "Dana Dibutuhkan",
|
||||
value: `Rp. ${
|
||||
(data && data?.targetDana && formatCurrencyDisplay(data?.targetDana)) ||
|
||||
"-"
|
||||
}`,
|
||||
},
|
||||
{
|
||||
label: "Harga Perlembar",
|
||||
value: `Rp. ${
|
||||
(data && data?.hargaLembar && formatCurrencyDisplay(data?.hargaLembar)) ||
|
||||
"-"
|
||||
}`,
|
||||
},
|
||||
{
|
||||
label: "Total Lembar",
|
||||
value:
|
||||
(data && data?.totalLembar && formatCurrencyDisplay(data?.totalLembar)) ||
|
||||
"-",
|
||||
},
|
||||
{
|
||||
label: "ROI",
|
||||
value: `${(data && data?.roi && data?.roi) || 0} %`,
|
||||
},
|
||||
{
|
||||
label: "Pembagian Deviden",
|
||||
value: (data && data?.MasterPembagianDeviden?.name) + " bulan" || "-",
|
||||
},
|
||||
{
|
||||
label: "Jadwal Pembagian",
|
||||
value: (data && data?.MasterPeriodeDeviden?.name) || "-",
|
||||
},
|
||||
{
|
||||
label: "Pencarian Investor",
|
||||
value: (data && data?.MasterPencarianInvestor?.name) + " hari" || "-",
|
||||
},
|
||||
];
|
||||
|
||||
@@ -13,6 +13,7 @@ import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButt
|
||||
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
|
||||
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import {
|
||||
apiAdminInvestmentGetOneInvoiceById,
|
||||
apiAdminInvestmentUpdateInvoice,
|
||||
@@ -25,6 +26,7 @@ import { useCallback, useState } from "react";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export default function AdminInvestmentTransactionDetail() {
|
||||
const { user } = useAuth();
|
||||
const { id } = useLocalSearchParams();
|
||||
const [data, setData] = useState<any | null>(null);
|
||||
const [isLoading, setLoading] = useState<boolean>(false);
|
||||
@@ -32,7 +34,7 @@ export default function AdminInvestmentTransactionDetail() {
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id])
|
||||
}, [id]),
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
@@ -40,7 +42,6 @@ export default function AdminInvestmentTransactionDetail() {
|
||||
const response = await apiAdminInvestmentGetOneInvoiceById({
|
||||
id: id as string,
|
||||
});
|
||||
// console.log("[RESPONSE]", JSON.stringify(response, null, 2));
|
||||
if (response.success) {
|
||||
setData(response.data);
|
||||
}
|
||||
@@ -92,7 +93,7 @@ export default function AdminInvestmentTransactionDetail() {
|
||||
<ButtonCustom
|
||||
onPress={() =>
|
||||
router.push(
|
||||
`/(application)/(image)/preview-image/${data?.imageId}`
|
||||
`/(application)/(image)/preview-image/${data?.imageId}`,
|
||||
)
|
||||
}
|
||||
>
|
||||
@@ -109,6 +110,13 @@ export default function AdminInvestmentTransactionDetail() {
|
||||
}: {
|
||||
category: "accept" | "deny";
|
||||
}) => {
|
||||
if (!user?.id) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Gagal update status transaksi",
|
||||
});
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await apiAdminInvestmentUpdateInvoice({
|
||||
@@ -117,11 +125,10 @@ export default function AdminInvestmentTransactionDetail() {
|
||||
data: {
|
||||
investasiId: data?.investasiId,
|
||||
lembarTerbeli: data?.lembarTerbeli,
|
||||
senderId: user?.id as any,
|
||||
},
|
||||
});
|
||||
|
||||
// console.log("[RESPONSE SUBMIT]", JSON.stringify(response, null, 2));
|
||||
|
||||
if (!response.success) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
@@ -153,6 +160,7 @@ export default function AdminInvestmentTransactionDetail() {
|
||||
styleRight={{ paddingLeft: 10 }}
|
||||
leftIcon={
|
||||
<ButtonCustom
|
||||
disabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
backgroundColor={MainColor.red}
|
||||
textColor="white"
|
||||
@@ -175,6 +183,7 @@ export default function AdminInvestmentTransactionDetail() {
|
||||
}
|
||||
rightIcon={
|
||||
<ButtonCustom
|
||||
disabled={isLoading}
|
||||
isLoading={isLoading}
|
||||
onPress={() => {
|
||||
AlertDefaultSystem({
|
||||
@@ -198,8 +207,8 @@ export default function AdminInvestmentTransactionDetail() {
|
||||
} else if (data?.StatusInvoice?.name === "Gagal") {
|
||||
return (
|
||||
<>
|
||||
<ButtonCustom textColor="red" onPress={() => router.back()}>
|
||||
Gagal
|
||||
<ButtonCustom disabled onPress={() => router.back()}>
|
||||
Transaksi telah gagal
|
||||
</ButtonCustom>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -7,34 +7,39 @@ import {
|
||||
} from "@/components";
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
|
||||
import { apiAdminInvestasiUpdateByStatus, apiAdminInvestmentDetailById } from "@/service/api-admin/api-admin-investment";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import {
|
||||
apiAdminInvestasiUpdateByStatus,
|
||||
apiAdminInvestmentDetailById,
|
||||
} from "@/service/api-admin/api-admin-investment";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export default function AdminInvestmentRejectInput() {
|
||||
const { user } = useAuth();
|
||||
const { id, status } = useLocalSearchParams();
|
||||
console.log("[STATUS]", status);
|
||||
const [value, setValue] = useState<any | null>(null);
|
||||
const [isLoading , setLoading] = useState(false)
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
const response = await apiAdminInvestmentDetailById({ id: id as string });
|
||||
console.log("[DATA]", JSON.stringify(response, null, 2));
|
||||
if (response.success) {
|
||||
setValue(response.data?.catatan);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [id])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
const response = await apiAdminInvestmentDetailById({ id: id as string });
|
||||
console.log("[DATA]", JSON.stringify(response, null, 2));
|
||||
if (response.success) {
|
||||
setValue(response.data?.catatan);
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
};
|
||||
|
||||
const handlerSubmit = async () => {
|
||||
if (!value) {
|
||||
@@ -45,12 +50,23 @@ export default function AdminInvestmentRejectInput() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!user?.id) {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "User tidak ditemukan",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true)
|
||||
setLoading(true);
|
||||
const response = await apiAdminInvestasiUpdateByStatus({
|
||||
id: id as string,
|
||||
status: "reject",
|
||||
data: value,
|
||||
data: {
|
||||
catatan: value,
|
||||
senderId: user?.id as string,
|
||||
},
|
||||
});
|
||||
|
||||
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
|
||||
@@ -76,7 +92,7 @@ export default function AdminInvestmentRejectInput() {
|
||||
} catch (error) {
|
||||
console.error(["ERROR"], error);
|
||||
} finally {
|
||||
setLoading(false)
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -22,8 +22,6 @@ import { Divider } from "react-native-paper";
|
||||
|
||||
export default function AdminInvestmentStatus() {
|
||||
const { status } = useLocalSearchParams();
|
||||
console.log("[STATUS]", status);
|
||||
|
||||
const [listData, setListData] = React.useState<any[] | null>(null);
|
||||
const [loadData, setLoadingData] = React.useState(false);
|
||||
const [search, setSearch] = React.useState("");
|
||||
@@ -41,7 +39,7 @@ export default function AdminInvestmentStatus() {
|
||||
category: status as "publish" | "review" | "reject",
|
||||
search,
|
||||
});
|
||||
console.log("[LIST DATA]", JSON.stringify(response, null, 2));
|
||||
|
||||
if (response.success) {
|
||||
setListData(response.data);
|
||||
}
|
||||
|
||||
@@ -1,213 +1,10 @@
|
||||
import {
|
||||
AlertDefaultSystem,
|
||||
BackButton,
|
||||
BaseBox,
|
||||
DrawerCustom,
|
||||
MenuDrawerDynamicGrid,
|
||||
NewWrapper,
|
||||
ScrollableCustom,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
} from "@/components";
|
||||
import { IconPlus } from "@/components/_Icon";
|
||||
import { IconDot } from "@/components/_Icon/IconComponent";
|
||||
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
|
||||
import NoDataText from "@/components/_ShareComponent/NoDataText";
|
||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useNotificationStore } from "@/hooks/use-notification-store";
|
||||
import { apiGetNotificationsById } from "@/service/api-notifications";
|
||||
import { listOfcategoriesAppNotification } from "@/types/type-notification-category";
|
||||
import { formatChatTime } from "@/utils/formatChatTime";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router, Stack, useFocusEffect } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { RefreshControl, View } from "react-native";
|
||||
|
||||
const selectedCategory = (value: string) => {
|
||||
const category = listOfcategoriesAppNotification.find(
|
||||
(c) => c.value === value
|
||||
);
|
||||
return category?.label;
|
||||
};
|
||||
|
||||
const BoxNotification = ({
|
||||
data,
|
||||
activeCategory,
|
||||
}: {
|
||||
data: any;
|
||||
activeCategory: string | null;
|
||||
}) => {
|
||||
const { markAsRead } = useNotificationStore();
|
||||
return (
|
||||
<>
|
||||
<BaseBox
|
||||
backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue}
|
||||
onPress={() => {
|
||||
console.log(
|
||||
"Notification >",
|
||||
selectedCategory(activeCategory as string)
|
||||
);
|
||||
router.push(data.deepLink);
|
||||
markAsRead(data.id);
|
||||
}}
|
||||
>
|
||||
<StackCustom>
|
||||
<TextCustom truncate={2} bold>
|
||||
{data.title}
|
||||
</TextCustom>
|
||||
|
||||
<TextCustom truncate={2}>{data.pesan}</TextCustom>
|
||||
|
||||
<TextCustom size="small" color="gray">
|
||||
{formatChatTime(data.createdAt)}
|
||||
</TextCustom>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
</>
|
||||
);
|
||||
};
|
||||
import Admin_ScreenNotification from "@/screens/Admin/Notification-Admin/ScreenNotificationAdmin";
|
||||
import Admin_ScreenNotification2 from "@/screens/Admin/Notification-Admin/ScreenNotificationAdmin2";
|
||||
|
||||
export default function AdminNotification() {
|
||||
const { user } = useAuth();
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>("event");
|
||||
const [listData, setListData] = useState<any[]>([]);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [openDrawer, setOpenDrawer] = useState(false);
|
||||
|
||||
const { markAsReadAll } = useNotificationStore();
|
||||
|
||||
const handlePress = (item: any) => {
|
||||
setActiveCategory(item.value);
|
||||
// tambahkan logika lain seperti filter dsb.
|
||||
};
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
fecthData();
|
||||
}, [activeCategory])
|
||||
);
|
||||
|
||||
const fecthData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await apiGetNotificationsById({
|
||||
id: user?.id as any,
|
||||
category: activeCategory as any,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setListData(response.data);
|
||||
} else {
|
||||
setListData([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error Notification", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onRefresh = () => {
|
||||
setRefreshing(true);
|
||||
fecthData();
|
||||
setRefreshing(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Admin Notifikasi",
|
||||
headerLeft: () => <BackButton />,
|
||||
headerRight: () => (
|
||||
<IconDot
|
||||
color={MainColor.yellow}
|
||||
onPress={() => setOpenDrawer(true)}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<NewWrapper
|
||||
headerComponent={
|
||||
<ScrollableCustom
|
||||
data={listOfcategoriesAppNotification.map((e, i) => ({
|
||||
id: i,
|
||||
label: e.label,
|
||||
value: e.value,
|
||||
}))}
|
||||
onButtonPress={handlePress}
|
||||
activeId={activeCategory as string}
|
||||
/>
|
||||
}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
||||
}
|
||||
>
|
||||
{loading ? (
|
||||
<ListSkeletonComponent />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<NoDataText text="Belum ada notifikasi" />
|
||||
) : (
|
||||
listData.map((e, i) => (
|
||||
<View key={i}>
|
||||
<BoxNotification
|
||||
data={e}
|
||||
activeCategory={activeCategory as any}
|
||||
/>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</NewWrapper>
|
||||
|
||||
<DrawerCustom
|
||||
isVisible={openDrawer}
|
||||
closeDrawer={() => setOpenDrawer(false)}
|
||||
height={"auto"}
|
||||
>
|
||||
<MenuDrawerDynamicGrid
|
||||
data={[
|
||||
{
|
||||
label: "Tandai Semua Dibaca",
|
||||
value: "read-all",
|
||||
icon: (
|
||||
<Ionicons
|
||||
name="reader-outline"
|
||||
size={ICON_SIZE_SMALL}
|
||||
color={MainColor.white}
|
||||
/>
|
||||
),
|
||||
path: "",
|
||||
},
|
||||
]}
|
||||
onPressItem={(item: any) => {
|
||||
console.log("Item", item.value);
|
||||
if (item.value === "read-all") {
|
||||
AlertDefaultSystem({
|
||||
title: "Tandai Semua Dibaca",
|
||||
message:
|
||||
"Apakah Anda yakin ingin menandai semua notifikasi dibaca?",
|
||||
textLeft: "Batal",
|
||||
textRight: "Ya",
|
||||
onPressRight: () => {
|
||||
markAsReadAll(user?.id as any);
|
||||
const data = _.cloneDeep(listData);
|
||||
data.forEach((e) => {
|
||||
e.isRead = true;
|
||||
});
|
||||
setListData(data);
|
||||
onRefresh();
|
||||
setOpenDrawer(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</DrawerCustom>
|
||||
<Admin_ScreenNotification2 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,14 +10,10 @@ import {
|
||||
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
|
||||
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { routeUser } from "@/lib/routeApp";
|
||||
import {
|
||||
apiAdminUserAccessGetById,
|
||||
apiAdminUserAccessUpdateStatus,
|
||||
} from "@/service/api-admin/api-admin-user-access";
|
||||
import {
|
||||
apiNotificationsSendById
|
||||
} from "@/service/api-notifications";
|
||||
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
import Toast from "react-native-toast-message";
|
||||
@@ -70,20 +66,6 @@ export default function AdminUserAccessDetail() {
|
||||
text1: "Update aktifasi berhasil ",
|
||||
});
|
||||
|
||||
if (data.active === false) {
|
||||
await apiNotificationsSendById({
|
||||
data: {
|
||||
title: "Akun anda telah diaktifkan",
|
||||
body: "Selamat menjelajahi HIConnect",
|
||||
userLoginId: user?.id || "",
|
||||
kategoriApp: "OTHER",
|
||||
type: "announcement",
|
||||
deepLink: routeUser.home,
|
||||
},
|
||||
id: id as string,
|
||||
});
|
||||
}
|
||||
|
||||
router.back();
|
||||
} catch (error) {
|
||||
console.log("[ERROR UPDATE STATUS]", error);
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
import { BackButton, StackCustom, TextCustom, ViewWrapper } from "@/components";
|
||||
import { Stack } from "expo-router";
|
||||
import { router, Stack } from "expo-router";
|
||||
|
||||
export default function NotFoundScreen() {
|
||||
// Setelah (dengan penanganan):
|
||||
const handleBack = () => {
|
||||
if (router.canGoBack()) {
|
||||
router.back();
|
||||
} else {
|
||||
// Alternatif action ketika tidak bisa kembali
|
||||
router.replace('/'); // atau navigasi ke halaman default
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{ headerShown: false, headerLeft: () => <BackButton /> }}
|
||||
options={{ headerShown: true, title: "", headerLeft: () => <BackButton onPress={() => handleBack()} /> }}
|
||||
/>
|
||||
<ViewWrapper>
|
||||
<StackCustom
|
||||
@@ -17,7 +27,7 @@ export default function NotFoundScreen() {
|
||||
404
|
||||
</TextCustom>
|
||||
<TextCustom size="large" bold>
|
||||
Sorry, File Not Found
|
||||
Sorry, Page Not Found
|
||||
</TextCustom>
|
||||
</StackCustom>
|
||||
</ViewWrapper>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
// DateTimeInput.tsx
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { GStyles } from "@/styles/global-styles";
|
||||
@@ -7,7 +6,14 @@ import DateTimePicker, {
|
||||
DateTimePickerEvent,
|
||||
} from "@react-native-community/datetimepicker";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { Pressable, StyleProp, Text, View, ViewStyle } from "react-native";
|
||||
import {
|
||||
Keyboard,
|
||||
Pressable,
|
||||
StyleProp,
|
||||
Text,
|
||||
View,
|
||||
ViewStyle,
|
||||
} from "react-native";
|
||||
import Grid from "../Grid/GridCustom";
|
||||
import TextCustom from "../Text/TextCustom";
|
||||
|
||||
@@ -53,7 +59,7 @@ const DateTimeInput_Android: React.FC<DateTimeInputProps> = ({
|
||||
const [selectedDate, setSelectedDate] = useState<Date>(value as any);
|
||||
const [selectedTime, setSelectedTime] = useState<Date>(value as any);
|
||||
|
||||
console.log("Date Android", value)
|
||||
console.log("Date Android", value);
|
||||
|
||||
// Fungsi untuk menggabungkan tanggal dan waktu
|
||||
const combineDateAndTime = useCallback((date: Date, time: Date): Date => {
|
||||
@@ -62,7 +68,7 @@ const DateTimeInput_Android: React.FC<DateTimeInputProps> = ({
|
||||
time.getHours(),
|
||||
time.getMinutes(),
|
||||
time.getSeconds(),
|
||||
time.getMilliseconds()
|
||||
time.getMilliseconds(),
|
||||
);
|
||||
return combined;
|
||||
}, []);
|
||||
@@ -92,10 +98,12 @@ const DateTimeInput_Android: React.FC<DateTimeInputProps> = ({
|
||||
};
|
||||
|
||||
const toggleDatePicker = () => {
|
||||
Keyboard.dismiss();
|
||||
setShowDate(!showDate);
|
||||
};
|
||||
|
||||
const toggleTimePicker = () => {
|
||||
Keyboard.dismiss();
|
||||
setShowTime(!showTime);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
// DateTimeInput.tsx
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||
import { GStyles } from "@/styles/global-styles";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import DateTimePicker, {
|
||||
DateTimePickerEvent,
|
||||
} from "@react-native-community/datetimepicker";
|
||||
import dayjs from "dayjs";
|
||||
import React, { useState } from "react";
|
||||
import { Button, StyleProp, Text, View, ViewStyle } from "react-native";
|
||||
import React, { useState, useRef } from "react";
|
||||
import {
|
||||
Button,
|
||||
StyleProp,
|
||||
Text,
|
||||
View,
|
||||
ViewStyle,
|
||||
Keyboard,
|
||||
TouchableOpacity,
|
||||
Modal,
|
||||
} from "react-native";
|
||||
import ClickableCustom from "../Clickable/ClickableCustom";
|
||||
import TextCustom from "../Text/TextCustom";
|
||||
|
||||
@@ -50,20 +59,35 @@ const DateTimeInput_IOS: React.FC<DateTimeInputProps> = ({
|
||||
}) => {
|
||||
const [show, setShow] = useState(false);
|
||||
const [selectedDate, setSelectedDate] = useState<Date | undefined>(
|
||||
value as any
|
||||
value as any,
|
||||
);
|
||||
// State sementara untuk menyimpan nilai yang dipilih
|
||||
const [tempSelectedDate, setTempSelectedDate] = useState<Date | undefined>(
|
||||
value as any,
|
||||
);
|
||||
|
||||
const handleConfirm = (event: any, date?: Date) => {
|
||||
if (event.type === "set" && date !== undefined) {
|
||||
setSelectedDate(date);
|
||||
onChange(date as any);
|
||||
// Hanya perbarui state sementara, bukan state utama
|
||||
setTempSelectedDate(date);
|
||||
}
|
||||
};
|
||||
|
||||
const handlePress = () => {
|
||||
// Sembunyikan keyboard sebelum menampilkan date picker
|
||||
Keyboard.dismiss();
|
||||
// Set state sementara ke nilai saat ini
|
||||
setTempSelectedDate(selectedDate);
|
||||
setShow(!show);
|
||||
};
|
||||
|
||||
// Fungsi untuk menangani klik di luar area picker
|
||||
const handleOutsidePress = () => {
|
||||
if (show) {
|
||||
setShow(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ClickableCustom
|
||||
@@ -112,84 +136,125 @@ const DateTimeInput_IOS: React.FC<DateTimeInputProps> = ({
|
||||
))}
|
||||
</ClickableCustom>
|
||||
|
||||
{show && (
|
||||
<>
|
||||
<View
|
||||
<Modal
|
||||
animationType="fade"
|
||||
transparent={true}
|
||||
visible={show}
|
||||
onRequestClose={() => setShow(false)}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)", // Efek blur dengan background semi-transparan
|
||||
}}
|
||||
>
|
||||
<TouchableOpacity
|
||||
style={{
|
||||
position: "absolute",
|
||||
zIndex: 15,
|
||||
backgroundColor: "white",
|
||||
borderRadius: 8,
|
||||
padding: 10,
|
||||
// top: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
borderColor: "#ccc",
|
||||
}}
|
||||
activeOpacity={1}
|
||||
onPress={handleOutsidePress}
|
||||
>
|
||||
<View style={{ flex: 1 }} />
|
||||
</TouchableOpacity>
|
||||
|
||||
<View
|
||||
style={{
|
||||
zIndex: 15,
|
||||
backgroundColor: MainColor.white_gray,
|
||||
borderRadius: 16,
|
||||
padding: 20,
|
||||
marginHorizontal: 20,
|
||||
width: "95%",
|
||||
maxWidth: 400,
|
||||
borderColor: MainColor.placeholder,
|
||||
borderWidth: 1,
|
||||
}}
|
||||
onStartShouldSetResponder={() => true} // Mencegah event bubbling ke TouchableOpacity induk
|
||||
onResponderRelease={() => {}} // Handler kosong untuk mencegah event bubbling
|
||||
>
|
||||
{/* <View style={{ alignItems: "flex-start" }}>
|
||||
<Ionicons
|
||||
name="close"
|
||||
size={20}
|
||||
color="black"
|
||||
onPress={() => {
|
||||
setShow(false);
|
||||
setSelectedDate(undefined);
|
||||
{/* <View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginBottom: 10,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 18,
|
||||
fontWeight: "bold",
|
||||
color: MainColor.black,
|
||||
}}
|
||||
/>
|
||||
>
|
||||
Pilih Tanggal & Waktu
|
||||
</Text>
|
||||
<TouchableOpacity onPress={() => setShow(false)}>
|
||||
<Ionicons
|
||||
name="close"
|
||||
size={24}
|
||||
color={AccentColor.blackgray}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
</View> */}
|
||||
|
||||
<DateTimePicker
|
||||
value={selectedDate || new Date()}
|
||||
value={tempSelectedDate || new Date()}
|
||||
mode={"datetime"}
|
||||
display="spinner"
|
||||
display="inline"
|
||||
onChange={handleConfirm}
|
||||
minimumDate={minimumDate}
|
||||
maximumDate={maximumDate}
|
||||
themeVariant="light"
|
||||
/>
|
||||
<View style={{ flexDirection: "row", gap: 10 }}>
|
||||
<ClickableCustom
|
||||
<View style={{ flexDirection: "row", gap: 10, marginTop: 15 }}>
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
setShow(false)
|
||||
setSelectedDate(undefined)
|
||||
setShow(false);
|
||||
// Kembalikan ke nilai sebelumnya jika batal
|
||||
setTempSelectedDate(selectedDate);
|
||||
}}
|
||||
style={{
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: 12,
|
||||
borderRadius: 10,
|
||||
backgroundColor: MainColor.placeholder,
|
||||
marginTop: 10,
|
||||
width: "48%",
|
||||
}}
|
||||
>
|
||||
<TextCustom color="black">Batal</TextCustom>
|
||||
</ClickableCustom>
|
||||
</TouchableOpacity>
|
||||
|
||||
<ClickableCustom
|
||||
<TouchableOpacity
|
||||
onPress={() => {
|
||||
setShow(false)
|
||||
onChange(selectedDate as any)
|
||||
// Simpan nilai yang dipilih ke state utama hanya saat OK ditekan
|
||||
setSelectedDate(tempSelectedDate);
|
||||
onChange(tempSelectedDate as any);
|
||||
setShow(false);
|
||||
}}
|
||||
style={{
|
||||
flex: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
padding: 12,
|
||||
borderRadius: 10,
|
||||
backgroundColor: MainColor.darkblue,
|
||||
marginTop: 10,
|
||||
width: "48%",
|
||||
}}
|
||||
>
|
||||
<TextCustom>OK</TextCustom>
|
||||
</ClickableCustom>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</View>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -30,7 +30,6 @@ export default function AvatarComp({
|
||||
href = `/(application)/(image)/preview-image/${fileId}`,
|
||||
}: AvatarCompProps) {
|
||||
const dimension = sizeMap[size];
|
||||
|
||||
const avatarImage = () => {
|
||||
return (
|
||||
<Avatar.Image
|
||||
@@ -52,8 +51,9 @@ export default function AvatarComp({
|
||||
<TouchableOpacity
|
||||
activeOpacity={0.9}
|
||||
onPress={
|
||||
href && fileId ? () => router.navigate(href as any) : onPress
|
||||
href || fileId ? () => router.navigate(href as any) : onPress
|
||||
}
|
||||
disabled={!fileId}
|
||||
>
|
||||
{avatarImage()}
|
||||
</TouchableOpacity>
|
||||
|
||||
33
components/Modal/ModalReactNative.tsx
Normal file
33
components/Modal/ModalReactNative.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Modal, View } from "react-native";
|
||||
|
||||
export default function ModalReactNative({
|
||||
children,
|
||||
isVisible,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
isVisible: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Modal
|
||||
animationType="slide"
|
||||
backdropColor={"rgba(0, 0, 0, 0.5)"}
|
||||
visible={isVisible}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.5)",
|
||||
// margin: 10,
|
||||
marginBlock: 30,
|
||||
padding: 10,
|
||||
borderRadius: 10,
|
||||
paddingTop: 30
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -49,9 +49,8 @@ export default function NotificationInitializer() {
|
||||
|
||||
const fcmToken = await getToken(messagingInstance);
|
||||
if (!fcmToken) {
|
||||
console.warn("Tidak bisa mendapatkan FCM token");
|
||||
console.log("Tidak bisa mendapatkan FCM token");
|
||||
// logout();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("✅ FCM Token:", fcmToken);
|
||||
|
||||
@@ -101,12 +101,14 @@ const NewWrapper = (props: NewWrapperProps) => {
|
||||
renderItem={listProps.renderItem}
|
||||
keyExtractor={
|
||||
listProps.keyExtractor ||
|
||||
((item) => {
|
||||
((item, index) => {
|
||||
if (item.id == null) {
|
||||
console.warn("Item tanpa 'id':", item);
|
||||
return `fallback-${JSON.stringify(item)}`;
|
||||
return `fallback-${index}-${JSON.stringify(item)}`;
|
||||
}
|
||||
return String(item.id);
|
||||
|
||||
// Gabungkan ID dengan indeks untuk mencegah duplikasi
|
||||
return `${String(item.id)}-${index}`;
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -19,11 +19,12 @@ export {
|
||||
PADDING_SMALL,
|
||||
PADDING_MEDIUM,
|
||||
PADDING_LARGE,
|
||||
PAGINATION_DEFAULT_TAKE
|
||||
};
|
||||
|
||||
// OS Height
|
||||
const OS_ANDROID_HEIGHT = 115
|
||||
const OS_IOS_HEIGHT = 70
|
||||
const OS_IOS_HEIGHT = 90
|
||||
const OS_HEIGHT = Platform.OS === "ios" ? OS_IOS_HEIGHT : OS_ANDROID_HEIGHT
|
||||
|
||||
// Text Size
|
||||
@@ -51,3 +52,5 @@ const PADDING_SMALL = 12
|
||||
const PADDING_MEDIUM = 16
|
||||
const PADDING_LARGE = 20
|
||||
|
||||
// Pagination
|
||||
const PAGINATION_DEFAULT_TAKE = 10;
|
||||
|
||||
@@ -21,7 +21,7 @@ type AuthContextType = {
|
||||
isAuthenticated: boolean;
|
||||
isAdmin: boolean;
|
||||
isUserActive: boolean;
|
||||
loginWithNomor: (nomor: string) => Promise<void>;
|
||||
loginWithNomor: (nomor: string) => Promise<boolean>;
|
||||
validateOtp: (nomor: string) => Promise<any>;
|
||||
logout: () => Promise<void>;
|
||||
registerUser: (userData: {
|
||||
@@ -30,12 +30,15 @@ type AuthContextType = {
|
||||
termsOfServiceAccepted: boolean;
|
||||
}) => Promise<void>;
|
||||
userData: (token: string) => Promise<any>;
|
||||
acceptedTerms: (nomor: string) => Promise<any>;
|
||||
acceptedTerms: (
|
||||
nomor: string,
|
||||
onSetModalVisible: (visible: boolean) => void,
|
||||
) => Promise<any>;
|
||||
};
|
||||
|
||||
// --- Create Context ---
|
||||
export const AuthContext = createContext<AuthContextType | undefined>(
|
||||
undefined
|
||||
undefined,
|
||||
);
|
||||
|
||||
export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
@@ -79,30 +82,12 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const response = await apiLogin({ nomor: nomor });
|
||||
console.log("[RESPONSE AUTH]", JSON.stringify(response, null, 2));
|
||||
|
||||
if (response.success) {
|
||||
if (response.isAcceptTerms) {
|
||||
Toast.show({
|
||||
type: "success",
|
||||
text1: "Sukses",
|
||||
text2: "Kode OTP berhasil dikirim",
|
||||
});
|
||||
|
||||
await AsyncStorage.setItem("kode_otp", response.kodeId);
|
||||
router.push(`/verification?nomor=${nomor}`);
|
||||
return;
|
||||
} else {
|
||||
router.push(`/eula?nomor=${nomor}`);
|
||||
return;
|
||||
}
|
||||
if (response.success && response.isAcceptTerms) {
|
||||
await AsyncStorage.setItem("kode_otp", response.kodeId);
|
||||
router.push(`/verification?nomor=${nomor}`);
|
||||
return true;
|
||||
} else {
|
||||
router.push(`/eula?nomor=${nomor}`);
|
||||
|
||||
// Toast.show({
|
||||
// type: "info",
|
||||
// text1: "Info",
|
||||
// text2: "Silahkan mendaftar",
|
||||
// });
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
} catch (error: any) {
|
||||
throw new Error(error.response?.data?.message || "Gagal kirim OTP");
|
||||
@@ -158,7 +143,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
} catch (error: any) {
|
||||
console.log("Error validasi otp >>", (error as Error).message || error);
|
||||
throw new Error(
|
||||
error.response?.data?.message || "OTP salah atau user tidak ditemukan"
|
||||
error.response?.data?.message || "OTP salah atau user tidak ditemukan",
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -187,7 +172,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
} catch (error: any) {
|
||||
console.log(
|
||||
"[LOAD USER DATA]",
|
||||
error.response?.data?.message + "user" || "Gagal mengambil data user"
|
||||
error.response?.data?.message + "user" || "Gagal mengambil data user",
|
||||
);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@@ -261,28 +246,25 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const acceptedTerms = async (nomor: string) => {
|
||||
// --- 6. Accept Terms ---
|
||||
const acceptedTerms = async (
|
||||
nomor: string,
|
||||
onSetModalVisible: (visible: boolean) => void,
|
||||
) => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
const response = await apiUpdatedTermCondition({ nomor: nomor });
|
||||
|
||||
if (response.success) {
|
||||
router.replace(`/verification?nomor=${nomor}`);
|
||||
return `/verification?nomor=${nomor}`;
|
||||
} else {
|
||||
if (response.status === 404) {
|
||||
router.replace(`/register?nomor=${nomor}`);
|
||||
} else {
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Error",
|
||||
text2: response.message,
|
||||
});
|
||||
}
|
||||
return `/register?nomor=${nomor}`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error accept terms", error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
onSetModalVisible(false);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ android: bunx expo prebuild --platform android
|
||||
adb devices : cek device yang terhubung
|
||||
Note: izinkan perangkat dulu agar statusnya tidak unauthorized
|
||||
|
||||
adb install android/app/build/outputs/apk/debug/app-debug.apk : install apk ke device
|
||||
adb install android/app/build/outputs/apk/debug/app-debug.apk : install apk ke device / emulator
|
||||
Note:
|
||||
Gunakan flag -s (serial) di perintah adb untuk menentukan target
|
||||
adb -s <0G52319V261040B2 ini adalah id nya> install android/app/build/outputs/apk/debug/app-debug.apk
|
||||
31
docs/prompt-for-qwen-code.md
Normal file
31
docs/prompt-for-qwen-code.md
Normal file
@@ -0,0 +1,31 @@
|
||||
<!-- ===================== Start Penerapan Pagination ===================== -->
|
||||
|
||||
File utama: screens/Voting/ScreenListOfContributor.tsx
|
||||
Function fecth: apiVotingContribution
|
||||
File function fetch: service/api-client/api-voting.ts
|
||||
File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
|
||||
|
||||
Terapkan pagination pada file "File utama"
|
||||
Analisa juga file "File utama" , jika belum menggunakan NewWrapper pada file "File komponen wrapper" , maka terapkan juga dan ganti wrapper lama yaitu komponen ViewWrapper
|
||||
|
||||
Komponen pagination yang digunaka berada pada file hooks/use-pagination.tsx dan helpers/paginationHelpers.tsx
|
||||
|
||||
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 -->
|
||||
File refrensi: app/(application)/(user)/event/[id]/list-of-participants.tsx
|
||||
Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan tipe fitur yang sama
|
||||
|
||||
<!-- ===================== End Penerapan Pagination ===================== -->
|
||||
|
||||
<!-- 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
|
||||
<!-- -->
|
||||
|
||||
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"
|
||||
jika tidak maka terapkan sesuai dengan logika yang sudah ada
|
||||
517
helpers/PaginationGuide.md
Normal file
517
helpers/PaginationGuide.md
Normal file
@@ -0,0 +1,517 @@
|
||||
# 📱 Reusable Pagination untuk React Native + Expo
|
||||
|
||||
Komponen pagination yang terintegrasi dengan **NewWrapper** untuk infinite scroll, pull-to-refresh, skeleton loading, dan empty state.
|
||||
|
||||
---
|
||||
|
||||
## 📦 File Structure
|
||||
|
||||
```
|
||||
/hooks/
|
||||
└── usePagination.tsx # Custom hook untuk logika pagination
|
||||
|
||||
/helpers/
|
||||
└── paginationHelpers.tsx # Helper functions untuk komponen UI
|
||||
|
||||
/components/
|
||||
└── NewWrapper.tsx # Komponen wrapper utama (existing)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Cara Penggunaan
|
||||
|
||||
### **Step 1: Import Hook dan Helpers**
|
||||
|
||||
```tsx
|
||||
import { usePagination } from "@/hooks/usePagination";
|
||||
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||
```
|
||||
|
||||
### **Step 2: Setup Pagination Hook**
|
||||
|
||||
```tsx
|
||||
const pagination = usePagination({
|
||||
// ✅ Fungsi untuk fetch data (harus return { data: T[] })
|
||||
fetchFunction: async (page, searchQuery) => {
|
||||
return await apiForumGetAll({
|
||||
category: "beranda",
|
||||
search: searchQuery || "",
|
||||
userLoginId: user.id,
|
||||
page: String(page),
|
||||
});
|
||||
},
|
||||
|
||||
// ✅ Page size (harus sama dengan API)
|
||||
pageSize: 5,
|
||||
|
||||
// ✅ Query pencarian
|
||||
searchQuery: search,
|
||||
|
||||
// ✅ Dependencies (reload saat berubah)
|
||||
dependencies: [user?.id, category],
|
||||
|
||||
// ⚙️ Optional callbacks
|
||||
onDataFetched: (data) => console.log("Loaded:", data.length),
|
||||
onError: (error) => console.error("Error:", error),
|
||||
});
|
||||
```
|
||||
|
||||
### **Step 3: Generate Komponen Pagination**
|
||||
|
||||
```tsx
|
||||
const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
searchQuery: search,
|
||||
emptyMessage: "Tidak ada data",
|
||||
emptySearchMessage: "Tidak ada hasil pencarian",
|
||||
skeletonCount: 5,
|
||||
skeletonHeight: 200,
|
||||
});
|
||||
```
|
||||
|
||||
### **Step 4: Gunakan dengan NewWrapper**
|
||||
|
||||
```tsx
|
||||
<NewWrapper
|
||||
// Props dari pagination hook
|
||||
listData={pagination.listData}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
/>
|
||||
}
|
||||
onEndReached={pagination.loadMore}
|
||||
|
||||
// Komponen dari helpers
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
|
||||
// Render item
|
||||
renderItem={({ item }) => <YourComponent data={item} />}
|
||||
|
||||
// Props lain dari NewWrapper
|
||||
headerComponent={<SearchInput />}
|
||||
floatingButton={<FloatingButton />}
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📖 Contoh Implementasi Lengkap
|
||||
|
||||
### **Contoh 1: Forum Page (Basic)**
|
||||
|
||||
```tsx
|
||||
import { usePagination } from "@/hooks/usePagination";
|
||||
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
|
||||
export default function ForumPage() {
|
||||
const { user } = useAuth();
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
// Setup pagination
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page, searchQuery) => {
|
||||
if (!user?.id) return { data: [] };
|
||||
|
||||
return await apiForumGetAll({
|
||||
category: "beranda",
|
||||
search: searchQuery || "",
|
||||
userLoginId: user.id,
|
||||
page: String(page),
|
||||
});
|
||||
},
|
||||
pageSize: 5,
|
||||
searchQuery: search,
|
||||
dependencies: [user?.id],
|
||||
});
|
||||
|
||||
// Generate komponen
|
||||
const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
searchQuery: search,
|
||||
emptyMessage: "Tidak ada diskusi",
|
||||
emptySearchMessage: "Tidak ada hasil pencarian",
|
||||
});
|
||||
|
||||
return (
|
||||
<NewWrapper
|
||||
headerComponent={
|
||||
<SearchInput
|
||||
placeholder="Cari diskusi..."
|
||||
onChangeText={_.debounce(setSearch, 500)}
|
||||
/>
|
||||
}
|
||||
listData={pagination.listData}
|
||||
renderItem={({ item }) => <ForumItem data={item} />}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
tintColor={MainColor.yellow}
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
/>
|
||||
}
|
||||
onEndReached={pagination.loadMore}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### **Contoh 2: Product Page (Dengan Filter)**
|
||||
|
||||
```tsx
|
||||
export default function ProductPage() {
|
||||
const [search, setSearch] = useState("");
|
||||
const [category, setCategory] = useState("all");
|
||||
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page, searchQuery) => {
|
||||
return await apiProductGetAll({
|
||||
page: String(page),
|
||||
search: searchQuery || "",
|
||||
category: category !== "all" ? category : undefined,
|
||||
});
|
||||
},
|
||||
pageSize: 10,
|
||||
searchQuery: search,
|
||||
dependencies: [category], // Reload saat category berubah
|
||||
});
|
||||
|
||||
const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
searchQuery: search,
|
||||
emptyMessage: "Belum ada produk",
|
||||
skeletonCount: 8,
|
||||
skeletonHeight: 100,
|
||||
});
|
||||
|
||||
return (
|
||||
<NewWrapper
|
||||
headerComponent={
|
||||
<View>
|
||||
<SearchInput onChangeText={setSearch} />
|
||||
<CategoryFilter value={category} onChange={setCategory} />
|
||||
</View>
|
||||
}
|
||||
listData={pagination.listData}
|
||||
renderItem={({ item }) => <ProductCard product={item} />}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
/>
|
||||
}
|
||||
onEndReached={pagination.loadMore}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ API Reference
|
||||
|
||||
### **usePagination Hook**
|
||||
|
||||
#### Props
|
||||
|
||||
| Prop | Type | Required | Default | Deskripsi |
|
||||
|------|------|----------|---------|-----------|
|
||||
| `fetchFunction` | `(page, search?) => Promise<{data: T[]}>` | ✅ | - | Fungsi fetch data dari API |
|
||||
| `pageSize` | `number` | ❌ | `5` | Jumlah data per halaman |
|
||||
| `searchQuery` | `string` | ❌ | `""` | Query pencarian |
|
||||
| `dependencies` | `any[]` | ❌ | `[]` | Dependencies untuk trigger reload |
|
||||
| `onDataFetched` | `(data: T[]) => void` | ❌ | - | Callback saat data berhasil di-fetch |
|
||||
| `onError` | `(error: any) => void` | ❌ | - | Callback saat terjadi error |
|
||||
|
||||
#### Return Value
|
||||
|
||||
```tsx
|
||||
{
|
||||
listData: T[]; // Array data untuk NewWrapper
|
||||
loading: boolean; // Loading state
|
||||
refreshing: boolean; // Refreshing state
|
||||
hasMore: boolean; // Apakah masih ada data
|
||||
page: number; // Current page
|
||||
onRefresh: () => void; // Function untuk refresh
|
||||
loadMore: () => void; // Function untuk load more
|
||||
reset: () => void; // Function untuk reset state
|
||||
setListData: (data) => void; // Function untuk set data manual
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **createPaginationComponents Helper**
|
||||
|
||||
#### Props
|
||||
|
||||
| Prop | Type | Required | Default | Deskripsi |
|
||||
|------|------|----------|---------|-----------|
|
||||
| `loading` | `boolean` | ✅ | - | Loading state |
|
||||
| `refreshing` | `boolean` | ✅ | - | Refreshing state |
|
||||
| `listData` | `any[]` | ✅ | - | List data |
|
||||
| `searchQuery` | `string` | ❌ | `""` | Query pencarian |
|
||||
| `emptyMessage` | `string` | ❌ | `"Tidak ada data"` | Pesan empty state |
|
||||
| `emptySearchMessage` | `string` | ❌ | `"Tidak ada hasil pencarian"` | Pesan empty saat search |
|
||||
| `skeletonCount` | `number` | ❌ | `5` | Jumlah skeleton items |
|
||||
| `skeletonHeight` | `number` | ❌ | `200` | Tinggi skeleton items |
|
||||
| `loadingFooterText` | `string` | ❌ | - | Text loading footer |
|
||||
|
||||
#### Return Value
|
||||
|
||||
```tsx
|
||||
{
|
||||
ListEmptyComponent: React.ReactElement; // Component untuk empty state
|
||||
ListFooterComponent: React.ReactElement; // Component untuk loading footer
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **Helper Functions Lain**
|
||||
|
||||
#### `createSkeletonList(options)`
|
||||
Generate skeleton list untuk loading state.
|
||||
|
||||
```tsx
|
||||
const SkeletonComponent = createSkeletonList({
|
||||
count: 5,
|
||||
height: 200
|
||||
});
|
||||
```
|
||||
|
||||
#### `createEmptyState(options)`
|
||||
Generate empty state component.
|
||||
|
||||
```tsx
|
||||
const EmptyComponent = createEmptyState({
|
||||
message: "Tidak ada data",
|
||||
searchMessage: "Tidak ada hasil pencarian",
|
||||
searchQuery: search
|
||||
});
|
||||
```
|
||||
|
||||
#### `createLoadingFooter(options)`
|
||||
Generate loading footer component.
|
||||
|
||||
```tsx
|
||||
const FooterComponent = createLoadingFooter({
|
||||
show: loading && listData.length > 0,
|
||||
text: "Memuat data..."
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 Custom Components
|
||||
|
||||
### **Custom Empty State**
|
||||
|
||||
```tsx
|
||||
import { createSkeletonList } from "@/helpers/paginationHelpers";
|
||||
|
||||
const CustomEmpty = (
|
||||
<View style={{ flex: 1, justifyContent: "center", alignItems: "center" }}>
|
||||
<Text>🔍</Text>
|
||||
<TextCustom>Data tidak ditemukan</TextCustom>
|
||||
</View>
|
||||
);
|
||||
|
||||
const ListEmptyComponent =
|
||||
pagination.loading && pagination.listData.length === 0
|
||||
? createSkeletonList({ count: 5, height: 200 })
|
||||
: CustomEmpty;
|
||||
|
||||
<NewWrapper
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
// ...
|
||||
/>
|
||||
```
|
||||
|
||||
### **Custom Loading Footer**
|
||||
|
||||
```tsx
|
||||
import { createLoadingFooter } from "@/helpers/paginationHelpers";
|
||||
|
||||
const CustomFooter = createLoadingFooter({
|
||||
show: pagination.loading && !pagination.refreshing && pagination.listData.length > 0,
|
||||
customComponent: (
|
||||
<View style={{ padding: 20, alignItems: "center" }}>
|
||||
<ActivityIndicator size="large" color="#007AFF" />
|
||||
<Text style={{ marginTop: 8 }}>Loading more...</Text>
|
||||
</View>
|
||||
)
|
||||
});
|
||||
|
||||
<NewWrapper
|
||||
ListFooterComponent={CustomFooter}
|
||||
// ...
|
||||
/>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✨ Fitur-Fitur
|
||||
|
||||
✅ **Infinite Scroll** - Auto load saat scroll ke bawah
|
||||
✅ **Pull to Refresh** - Swipe down untuk refresh
|
||||
✅ **Skeleton Loading** - Smooth loading animation
|
||||
✅ **Empty State** - Tampilan saat data kosong
|
||||
✅ **Search Integration** - Support search dengan debounce
|
||||
✅ **Multi Dependencies** - Reload berdasarkan filter apapun
|
||||
✅ **Error Handling** - Built-in error handling
|
||||
✅ **TypeScript** - Full type safety
|
||||
✅ **Fully Customizable** - Custom components untuk semua state
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Best Practices
|
||||
|
||||
### 1. **Gunakan Debounce untuk Search**
|
||||
```tsx
|
||||
<SearchInput
|
||||
onChangeText={_.debounce((text) => setSearch(text), 500)}
|
||||
/>
|
||||
```
|
||||
|
||||
### 2. **Sesuaikan Page Size dengan API**
|
||||
```tsx
|
||||
const pagination = usePagination({
|
||||
pageSize: 5, // Harus sama dengan takeData di API
|
||||
});
|
||||
```
|
||||
|
||||
### 3. **Tambahkan Dependencies yang Relevan**
|
||||
```tsx
|
||||
const pagination = usePagination({
|
||||
dependencies: [userId, category, sortBy], // Reload saat berubah
|
||||
});
|
||||
```
|
||||
|
||||
### 4. **Handle Error dengan Baik**
|
||||
```tsx
|
||||
const pagination = usePagination({
|
||||
onError: (error) => {
|
||||
console.error("Error:", error);
|
||||
Alert.alert("Error", "Gagal memuat data");
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 5. **Pastikan API Return Format yang Benar**
|
||||
```tsx
|
||||
// ❌ SALAH
|
||||
fetchFunction: async () => [data1, data2];
|
||||
|
||||
// ✅ BENAR
|
||||
fetchFunction: async () => ({ data: [data1, data2] });
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Troubleshooting
|
||||
|
||||
### **Data tidak muncul?**
|
||||
- Pastikan `fetchFunction` return `{ data: T[] }`
|
||||
- Cek apakah API return format yang benar
|
||||
- Pastikan `pageSize` sesuai dengan API
|
||||
|
||||
### **Infinite scroll tidak jalan?**
|
||||
- Pastikan API return data sesuai `pageSize`
|
||||
- Cek `hasMore` state
|
||||
- Pastikan `onEndReachedThreshold` tidak terlalu kecil (default 0.5)
|
||||
|
||||
### **Skeleton terus muncul?**
|
||||
- Cek `loading` state
|
||||
- Pastikan `fetchFunction` resolve dengan benar
|
||||
- Cek error di console
|
||||
|
||||
### **Refresh tidak bekerja?**
|
||||
- Pastikan `RefreshControl` menggunakan `pagination.refreshing` dan `pagination.onRefresh`
|
||||
- Cek apakah API dipanggil saat pull-to-refresh
|
||||
|
||||
---
|
||||
|
||||
## 📝 Migration Guide
|
||||
|
||||
### **Dari Code Lama ke Code Baru**
|
||||
|
||||
#### **BEFORE:**
|
||||
```tsx
|
||||
const [listData, setListData] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
const fetchData = async (pageNumber, clear) => {
|
||||
// ... 30+ lines of code
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
setListData([]);
|
||||
setHasMore(true);
|
||||
fetchData(1, true);
|
||||
}, [search, user?.id]);
|
||||
|
||||
const onRefresh = useCallback(() => {
|
||||
fetchData(1, true);
|
||||
}, [search, user?.id]);
|
||||
|
||||
const loadMore = useCallback(() => {
|
||||
if (hasMore && !loading && !refreshing) {
|
||||
fetchData(page + 1, false);
|
||||
}
|
||||
}, [hasMore, loading, refreshing, page, search, user?.id]);
|
||||
|
||||
// ... skeleton, empty, footer components
|
||||
```
|
||||
|
||||
#### **AFTER:**
|
||||
```tsx
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page, search) => await apiGetData({ page, search }),
|
||||
pageSize: 5,
|
||||
searchQuery: search,
|
||||
dependencies: [user?.id]
|
||||
});
|
||||
|
||||
const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
searchQuery: search,
|
||||
});
|
||||
```
|
||||
|
||||
**Result:** 50+ lines → 15 lines! 🎉
|
||||
|
||||
---
|
||||
|
||||
## 👨💻 Author
|
||||
|
||||
Created by Full-Stack Developer
|
||||
React Native + Expo Specialist
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
MIT License - Feel free to use in your projects!
|
||||
280
helpers/paginationHelpers.tsx
Normal file
280
helpers/paginationHelpers.tsx
Normal file
@@ -0,0 +1,280 @@
|
||||
import { View } from "react-native";
|
||||
import { LoaderCustom, TextCustom, StackCustom } from "@/components";
|
||||
import SkeletonCustom from "@/components/_ShareComponent/SkeletonCustom";
|
||||
import _ from "lodash";
|
||||
|
||||
/**
|
||||
* Pagination Helpers
|
||||
*
|
||||
* Helper functions untuk membuat komponen-komponen pagination
|
||||
* yang sering digunakan (Skeleton, Empty State, Loading Footer)
|
||||
*/
|
||||
|
||||
interface SkeletonListOptions {
|
||||
/**
|
||||
* Jumlah skeleton items
|
||||
* @default 5
|
||||
*/
|
||||
count?: number;
|
||||
|
||||
/**
|
||||
* Tinggi setiap skeleton item
|
||||
* @default 200
|
||||
*/
|
||||
height?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Skeleton List Component untuk loading state
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <NewWrapper
|
||||
* listData={listData}
|
||||
* ListEmptyComponent={
|
||||
* loading && _.isEmpty(listData)
|
||||
* ? createSkeletonList({ count: 5, height: 200 })
|
||||
* : createEmptyState({ message: "Tidak ada data" })
|
||||
* }
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export const createSkeletonList = (options: SkeletonListOptions = {}) => {
|
||||
const { count = 5, height = 200 } = options;
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1 }}>
|
||||
<StackCustom>
|
||||
{Array.from({ length: count }).map((_, i) => (
|
||||
<SkeletonCustom height={height} key={i} />
|
||||
))}
|
||||
</StackCustom>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
interface EmptyStateOptions {
|
||||
/**
|
||||
* Pesan untuk empty state
|
||||
* @default "Tidak ada data"
|
||||
*/
|
||||
message?: string;
|
||||
|
||||
/**
|
||||
* Pesan untuk empty state saat search
|
||||
*/
|
||||
searchMessage?: string;
|
||||
|
||||
/**
|
||||
* Query pencarian (untuk menentukan pesan mana yang ditampilkan)
|
||||
*/
|
||||
searchQuery?: string;
|
||||
|
||||
/**
|
||||
* Custom component untuk empty state
|
||||
*/
|
||||
customComponent?: React.ReactElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Empty State Component
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* ListEmptyComponent={
|
||||
* createEmptyState({
|
||||
* message: "Tidak ada diskusi",
|
||||
* searchMessage: "Tidak ada hasil pencarian",
|
||||
* searchQuery: search
|
||||
* })
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const createEmptyState = (options: EmptyStateOptions = {}) => {
|
||||
const {
|
||||
message = "Tidak ada data",
|
||||
searchMessage = "Tidak ada hasil pencarian",
|
||||
searchQuery = "",
|
||||
customComponent,
|
||||
} = options;
|
||||
|
||||
if (customComponent) return customComponent;
|
||||
|
||||
return (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
padding: 20,
|
||||
}}
|
||||
>
|
||||
<TextCustom align="center" color="gray">
|
||||
{searchQuery ? searchMessage : message}
|
||||
</TextCustom>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
interface LoadingFooterOptions {
|
||||
/**
|
||||
* Tampilkan loading footer
|
||||
*/
|
||||
show: boolean;
|
||||
|
||||
/**
|
||||
* Custom text untuk loading footer
|
||||
*/
|
||||
text?: string;
|
||||
|
||||
/**
|
||||
* Custom component untuk loading footer
|
||||
*/
|
||||
customComponent?: React.ReactElement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate Loading Footer Component
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* ListFooterComponent={
|
||||
* createLoadingFooter({
|
||||
* show: loading && !refreshing && listData.length > 0,
|
||||
* text: "Memuat data..."
|
||||
* })
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export const createLoadingFooter = (options: LoadingFooterOptions) => {
|
||||
const { show, text, customComponent } = options;
|
||||
|
||||
if (!show) return null;
|
||||
|
||||
if (customComponent) return customComponent;
|
||||
|
||||
return (
|
||||
<View style={{ paddingVertical: 16, alignItems: "center" }}>
|
||||
{text ? (
|
||||
<TextCustom color="gray">
|
||||
{text}
|
||||
</TextCustom>
|
||||
) : (
|
||||
<LoaderCustom />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
interface PaginationComponentsOptions {
|
||||
/**
|
||||
* Loading state
|
||||
*/
|
||||
loading: boolean;
|
||||
|
||||
/**
|
||||
* Refreshing state
|
||||
*/
|
||||
refreshing: boolean;
|
||||
|
||||
/**
|
||||
* List data
|
||||
*/
|
||||
listData: any[];
|
||||
|
||||
/**
|
||||
* Query pencarian
|
||||
*/
|
||||
searchQuery?: string;
|
||||
|
||||
/**
|
||||
* Pesan empty state
|
||||
*/
|
||||
emptyMessage?: string;
|
||||
|
||||
/**
|
||||
* Pesan empty state saat search
|
||||
*/
|
||||
emptySearchMessage?: string;
|
||||
|
||||
/**
|
||||
* Jumlah skeleton items
|
||||
*/
|
||||
skeletonCount?: number;
|
||||
|
||||
/**
|
||||
* Tinggi skeleton items
|
||||
*/
|
||||
skeletonHeight?: number;
|
||||
|
||||
/**
|
||||
* Text loading footer
|
||||
*/
|
||||
loadingFooterText?: string;
|
||||
|
||||
/**
|
||||
* Loading pertama
|
||||
*/
|
||||
isInitialLoad?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate semua komponen pagination sekaligus
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
|
||||
* loading,
|
||||
* refreshing,
|
||||
* listData,
|
||||
* searchQuery: search,
|
||||
* emptyMessage: "Tidak ada diskusi",
|
||||
* emptySearchMessage: "Tidak ada hasil pencarian",
|
||||
* skeletonCount: 5,
|
||||
* skeletonHeight: 200
|
||||
* });
|
||||
*
|
||||
* <NewWrapper
|
||||
* listData={listData}
|
||||
* ListEmptyComponent={ListEmptyComponent}
|
||||
* ListFooterComponent={ListFooterComponent}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export const createPaginationComponents = (
|
||||
options: PaginationComponentsOptions
|
||||
) => {
|
||||
const {
|
||||
loading,
|
||||
refreshing,
|
||||
listData,
|
||||
searchQuery = "",
|
||||
emptyMessage = "Tidak ada data",
|
||||
emptySearchMessage = "Tidak ada hasil pencarian",
|
||||
skeletonCount = 5,
|
||||
skeletonHeight = 200,
|
||||
loadingFooterText,
|
||||
isInitialLoad,
|
||||
} = options;
|
||||
|
||||
// Empty Compotnent: Skeleton saat loading pertama, Empty State saat data kosong
|
||||
const ListEmptyComponent =
|
||||
loading && _.isEmpty(listData)
|
||||
? createSkeletonList({ count: skeletonCount, height: skeletonHeight })
|
||||
: createEmptyState({
|
||||
message: emptyMessage,
|
||||
searchMessage: emptySearchMessage,
|
||||
searchQuery,
|
||||
});
|
||||
|
||||
// Footer Component: Loading indicator saat load more
|
||||
const ListFooterComponent = createLoadingFooter({
|
||||
show: loading && !refreshing && listData.length > 0,
|
||||
text: loadingFooterText,
|
||||
});
|
||||
|
||||
return {
|
||||
ListEmptyComponent,
|
||||
ListFooterComponent,
|
||||
};
|
||||
};
|
||||
184
hooks/use-pagination.tsx
Normal file
184
hooks/use-pagination.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
|
||||
interface UsePaginationProps<T> {
|
||||
/**
|
||||
* Fungsi API untuk fetch data
|
||||
* @param page - nomor halaman
|
||||
* @param search - query pencarian (opsional)
|
||||
* @returns Promise dengan response API (bukan langsung array)
|
||||
*/
|
||||
fetchFunction: (page: number, search?: string) => Promise<{ data: T[] }>;
|
||||
|
||||
/**
|
||||
* Jumlah data per halaman (harus sama dengan API)
|
||||
* @default 5
|
||||
*/
|
||||
pageSize?: number;
|
||||
|
||||
/**
|
||||
* Query pencarian
|
||||
*/
|
||||
searchQuery?: string;
|
||||
|
||||
/**
|
||||
* Dependencies tambahan untuk trigger reload
|
||||
* Contoh: [userId, categoryId]
|
||||
*/
|
||||
dependencies?: any[];
|
||||
|
||||
/**
|
||||
* Callback saat data berhasil di-fetch
|
||||
*/
|
||||
onDataFetched?: (data: T[]) => void;
|
||||
|
||||
/**
|
||||
* Callback saat terjadi error
|
||||
*/
|
||||
onError?: (error: any) => void;
|
||||
}
|
||||
|
||||
interface UsePaginationReturn<T> {
|
||||
// Data state
|
||||
listData: T[];
|
||||
loading: boolean;
|
||||
refreshing: boolean;
|
||||
hasMore: boolean;
|
||||
page: number;
|
||||
|
||||
// Actions
|
||||
onRefresh: () => void;
|
||||
loadMore: () => void;
|
||||
reset: () => void;
|
||||
setListData: React.Dispatch<React.SetStateAction<T[]>>;
|
||||
isInitialLoad: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom Hook untuk menangani pagination dengan infinite scroll
|
||||
*
|
||||
* Hook ini mengembalikan props yang siap digunakan langsung dengan NewWrapper
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* const pagination = usePagination({
|
||||
* fetchFunction: async (page, search) => {
|
||||
* return await apiForumGetAll({
|
||||
* category: "beranda",
|
||||
* search: search || "",
|
||||
* userLoginId: user.id,
|
||||
* page: String(page),
|
||||
* });
|
||||
* },
|
||||
* pageSize: 5,
|
||||
* searchQuery: search,
|
||||
* dependencies: [user?.id]
|
||||
* });
|
||||
*
|
||||
* // Lalu gunakan langsung di NewWrapper:
|
||||
* <NewWrapper
|
||||
* listData={pagination.listData}
|
||||
* refreshControl={<RefreshControl refreshing={pagination.refreshing} onRefresh={pagination.onRefresh} />}
|
||||
* onEndReached={pagination.loadMore}
|
||||
* // ... props lainnya
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export function usePagination<T = any>({
|
||||
fetchFunction,
|
||||
pageSize = 5,
|
||||
searchQuery = "",
|
||||
dependencies = [],
|
||||
onDataFetched,
|
||||
onError,
|
||||
}: UsePaginationProps<T>): UsePaginationReturn<T> {
|
||||
const [listData, setListData] = useState<T[]>([]);
|
||||
const [loading, setLoading] = useState(true); // Set true untuk initial load
|
||||
const [isInitialLoad, setIsInitialLoad] = useState(true); // Track initial load
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [page, setPage] = useState(1);
|
||||
|
||||
/**
|
||||
* Fungsi utama untuk fetch data
|
||||
*/
|
||||
const fetchData = async (pageNumber: number, clear: boolean) => {
|
||||
// Cegah multiple call
|
||||
if (!clear && (loading || refreshing)) return;
|
||||
|
||||
const isRefresh = clear;
|
||||
if (isRefresh) setRefreshing(true);
|
||||
if (!isRefresh) setLoading(true);
|
||||
|
||||
try {
|
||||
const response = await fetchFunction(pageNumber, searchQuery);
|
||||
const newData = response.data || [];
|
||||
// console.log("newData", newData);
|
||||
setListData((prev) => {
|
||||
const current = Array.isArray(prev) ? prev : [];
|
||||
return clear ? newData : [...current, ...newData];
|
||||
});
|
||||
// setTimeout(() => {
|
||||
// }, 4000);
|
||||
|
||||
setHasMore(newData.length === pageSize);
|
||||
setPage(pageNumber);
|
||||
|
||||
// Callback jika ada
|
||||
onDataFetched?.(newData);
|
||||
} catch (error) {
|
||||
console.error("[usePagination] Error fetching data:", error);
|
||||
setHasMore(false);
|
||||
onError?.(error);
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
setLoading(false);
|
||||
setIsInitialLoad(false); // Set false setelah initial load
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Reset dan reload saat search atau dependencies berubah
|
||||
*/
|
||||
useEffect(() => {
|
||||
reset();
|
||||
fetchData(1, true);
|
||||
}, [searchQuery, ...dependencies]);
|
||||
|
||||
/**
|
||||
* Pull-to-refresh
|
||||
*/
|
||||
const onRefresh = useCallback(() => {
|
||||
fetchData(1, true);
|
||||
}, [searchQuery, ...dependencies]);
|
||||
|
||||
/**
|
||||
* Load more (infinite scroll)
|
||||
*/
|
||||
const loadMore = useCallback(() => {
|
||||
if (hasMore && !loading && !refreshing) {
|
||||
fetchData(page + 1, false);
|
||||
}
|
||||
}, [hasMore, loading, refreshing, page, searchQuery, ...dependencies]);
|
||||
|
||||
/**
|
||||
* Reset state pagination
|
||||
*/
|
||||
const reset = useCallback(() => {
|
||||
setPage(1);
|
||||
setListData([]);
|
||||
setHasMore(true);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
listData,
|
||||
loading,
|
||||
refreshing,
|
||||
hasMore,
|
||||
page,
|
||||
onRefresh,
|
||||
loadMore,
|
||||
reset,
|
||||
setListData,
|
||||
isInitialLoad
|
||||
};
|
||||
}
|
||||
@@ -39,7 +39,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>19</string>
|
||||
<string>20</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { apiAdminDonationUpdateStatus } from "@/service/api-admin/api-admin-donation";
|
||||
import { typeRejectedData } from "@/types/type-collect-other";
|
||||
|
||||
export const funUpdateStatusDonation = async ({
|
||||
id,
|
||||
@@ -7,7 +8,7 @@ export const funUpdateStatusDonation = async ({
|
||||
}: {
|
||||
id: string;
|
||||
changeStatus: "publish" | "review" | "reject";
|
||||
data?: string;
|
||||
data?: typeRejectedData;
|
||||
}) => {
|
||||
try {
|
||||
const response = await apiAdminDonationUpdateStatus({
|
||||
|
||||
213
screens/Admin/Notification-Admin/ScreenNotificationAdmin.tsx
Normal file
213
screens/Admin/Notification-Admin/ScreenNotificationAdmin.tsx
Normal file
@@ -0,0 +1,213 @@
|
||||
import {
|
||||
AlertDefaultSystem,
|
||||
BackButton,
|
||||
BaseBox,
|
||||
DrawerCustom,
|
||||
MenuDrawerDynamicGrid,
|
||||
NewWrapper,
|
||||
ScrollableCustom,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
} from "@/components";
|
||||
import { IconPlus } from "@/components/_Icon";
|
||||
import { IconDot } from "@/components/_Icon/IconComponent";
|
||||
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
|
||||
import NoDataText from "@/components/_ShareComponent/NoDataText";
|
||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useNotificationStore } from "@/hooks/use-notification-store";
|
||||
import { apiGetNotificationsById } from "@/service/api-notifications";
|
||||
import { listOfcategoriesAppNotification } from "@/types/type-notification-category";
|
||||
import { formatChatTime } from "@/utils/formatChatTime";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router, Stack, useFocusEffect } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { RefreshControl, View } from "react-native";
|
||||
|
||||
const selectedCategory = (value: string) => {
|
||||
const category = listOfcategoriesAppNotification.find(
|
||||
(c) => c.value === value
|
||||
);
|
||||
return category?.label;
|
||||
};
|
||||
|
||||
const BoxNotification = ({
|
||||
data,
|
||||
activeCategory,
|
||||
}: {
|
||||
data: any;
|
||||
activeCategory: string | null;
|
||||
}) => {
|
||||
const { markAsRead } = useNotificationStore();
|
||||
return (
|
||||
<>
|
||||
<BaseBox
|
||||
backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue}
|
||||
onPress={() => {
|
||||
console.log(
|
||||
"Notification >",
|
||||
selectedCategory(activeCategory as string)
|
||||
);
|
||||
router.push(data.deepLink);
|
||||
markAsRead(data.id);
|
||||
}}
|
||||
>
|
||||
<StackCustom>
|
||||
<TextCustom truncate={2} bold>
|
||||
{data.title}
|
||||
</TextCustom>
|
||||
|
||||
<TextCustom truncate={2}>{data.pesan}</TextCustom>
|
||||
|
||||
<TextCustom size="small" color="gray">
|
||||
{formatChatTime(data.createdAt)}
|
||||
</TextCustom>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default function Admin_ScreenNotification() {
|
||||
const { user } = useAuth();
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>("event");
|
||||
const [listData, setListData] = useState<any[]>([]);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [openDrawer, setOpenDrawer] = useState(false);
|
||||
|
||||
const { markAsReadAll } = useNotificationStore();
|
||||
|
||||
const handlePress = (item: any) => {
|
||||
setActiveCategory(item.value);
|
||||
// tambahkan logika lain seperti filter dsb.
|
||||
};
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
fecthData();
|
||||
}, [activeCategory])
|
||||
);
|
||||
|
||||
const fecthData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await apiGetNotificationsById({
|
||||
id: user?.id as any,
|
||||
category: activeCategory as any,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setListData(response.data);
|
||||
} else {
|
||||
setListData([]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Error Notification", error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const onRefresh = () => {
|
||||
setRefreshing(true);
|
||||
fecthData();
|
||||
setRefreshing(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Admin Notifikasi",
|
||||
headerLeft: () => <BackButton />,
|
||||
headerRight: () => (
|
||||
<IconDot
|
||||
color={MainColor.yellow}
|
||||
onPress={() => setOpenDrawer(true)}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<NewWrapper
|
||||
headerComponent={
|
||||
<ScrollableCustom
|
||||
data={listOfcategoriesAppNotification.map((e, i) => ({
|
||||
id: i,
|
||||
label: e.label,
|
||||
value: e.value,
|
||||
}))}
|
||||
onButtonPress={handlePress}
|
||||
activeId={activeCategory as string}
|
||||
/>
|
||||
}
|
||||
refreshControl={
|
||||
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
|
||||
}
|
||||
>
|
||||
{loading ? (
|
||||
<ListSkeletonComponent />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<NoDataText text="Belum ada notifikasi" />
|
||||
) : (
|
||||
listData.map((e, i) => (
|
||||
<View key={i}>
|
||||
<BoxNotification
|
||||
data={e}
|
||||
activeCategory={activeCategory as any}
|
||||
/>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</NewWrapper>
|
||||
|
||||
<DrawerCustom
|
||||
isVisible={openDrawer}
|
||||
closeDrawer={() => setOpenDrawer(false)}
|
||||
height={"auto"}
|
||||
>
|
||||
<MenuDrawerDynamicGrid
|
||||
data={[
|
||||
{
|
||||
label: "Tandai Semua Dibaca",
|
||||
value: "read-all",
|
||||
icon: (
|
||||
<Ionicons
|
||||
name="reader-outline"
|
||||
size={ICON_SIZE_SMALL}
|
||||
color={MainColor.white}
|
||||
/>
|
||||
),
|
||||
path: "",
|
||||
},
|
||||
]}
|
||||
onPressItem={(item: any) => {
|
||||
console.log("Item", item.value);
|
||||
if (item.value === "read-all") {
|
||||
AlertDefaultSystem({
|
||||
title: "Tandai Semua Dibaca",
|
||||
message:
|
||||
"Apakah Anda yakin ingin menandai semua notifikasi dibaca?",
|
||||
textLeft: "Batal",
|
||||
textRight: "Ya",
|
||||
onPressRight: () => {
|
||||
markAsReadAll(user?.id as any);
|
||||
const data = _.cloneDeep(listData);
|
||||
data.forEach((e) => {
|
||||
e.isRead = true;
|
||||
});
|
||||
setListData(data);
|
||||
onRefresh();
|
||||
setOpenDrawer(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</DrawerCustom>
|
||||
</>
|
||||
);
|
||||
}
|
||||
256
screens/Admin/Notification-Admin/ScreenNotificationAdmin2.tsx
Normal file
256
screens/Admin/Notification-Admin/ScreenNotificationAdmin2.tsx
Normal file
@@ -0,0 +1,256 @@
|
||||
import {
|
||||
AlertDefaultSystem,
|
||||
BackButton,
|
||||
BaseBox,
|
||||
DrawerCustom,
|
||||
MenuDrawerDynamicGrid,
|
||||
NewWrapper,
|
||||
ScrollableCustom,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
} from "@/components";
|
||||
import { IconPlus } from "@/components/_Icon";
|
||||
import { IconDot } from "@/components/_Icon/IconComponent";
|
||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||
import { ICON_SIZE_SMALL, PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useNotificationStore } from "@/hooks/use-notification-store";
|
||||
import { usePagination } from "@/hooks/use-pagination";
|
||||
import { apiGetNotificationsById } from "@/service/api-notifications";
|
||||
import { listOfcategoriesAppNotification } from "@/types/type-notification-category";
|
||||
import { formatChatTime } from "@/utils/formatChatTime";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
import { router, Stack, useFocusEffect } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useState } from "react";
|
||||
import { RefreshControl, View } from "react-native";
|
||||
|
||||
const selectedCategory = (value: string) => {
|
||||
const category = listOfcategoriesAppNotification.find(
|
||||
(c) => c.value === value,
|
||||
);
|
||||
return category?.label;
|
||||
};
|
||||
|
||||
const fixPath = ({
|
||||
deepLink,
|
||||
categoryApp,
|
||||
}: {
|
||||
deepLink: string;
|
||||
categoryApp: string;
|
||||
}) => {
|
||||
// Jika categoryApp adalah "OTHER", kembalikan deepLink tanpa perubahan
|
||||
if (categoryApp === "OTHER") {
|
||||
return deepLink;
|
||||
}
|
||||
|
||||
// Jika dalam deepLink terdapat "/admin/", kembalikan path tersebut tanpa modifikasi tambahan
|
||||
if (deepLink.includes("/admin/")) {
|
||||
return deepLink;
|
||||
}
|
||||
|
||||
console.log("Category App", categoryApp);
|
||||
console.log("Deep Link", deepLink);
|
||||
|
||||
const separator = deepLink.includes("?") ? "&" : "?";
|
||||
|
||||
const fixedPath = `${deepLink}${separator}from=notifications&category=${_.lowerCase(
|
||||
categoryApp,
|
||||
)}`;
|
||||
|
||||
console.log("Fix Path", fixedPath);
|
||||
|
||||
return fixedPath;
|
||||
};
|
||||
|
||||
const BoxNotification = ({
|
||||
data,
|
||||
activeCategory,
|
||||
setListData,
|
||||
}: {
|
||||
data: any;
|
||||
activeCategory: string | null;
|
||||
setListData: (data: any) => void;
|
||||
}) => {
|
||||
const { markAsRead } = useNotificationStore();
|
||||
return (
|
||||
<>
|
||||
<BaseBox
|
||||
backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue}
|
||||
onPress={() => {
|
||||
const newPath = fixPath({
|
||||
deepLink: data.deepLink,
|
||||
categoryApp: data.kategoriApp,
|
||||
});
|
||||
|
||||
selectedCategory(activeCategory as string);
|
||||
router.navigate(newPath as any);
|
||||
|
||||
if (!data.isRead) {
|
||||
markAsRead(data.id);
|
||||
setListData((prev: any) =>
|
||||
prev.map((item: any) =>
|
||||
item.id === data.id ? { ...item, isRead: true } : item,
|
||||
),
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<StackCustom>
|
||||
<TextCustom truncate={2} bold>
|
||||
{data.title}
|
||||
</TextCustom>
|
||||
|
||||
<TextCustom truncate={2}>{data.pesan}</TextCustom>
|
||||
|
||||
<TextCustom size="small" color="gray">
|
||||
{formatChatTime(data.createdAt)}
|
||||
</TextCustom>
|
||||
</StackCustom>
|
||||
</BaseBox>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default function Admin_ScreenNotification2() {
|
||||
const { user } = useAuth();
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>("event");
|
||||
const [openDrawer, setOpenDrawer] = useState(false);
|
||||
|
||||
const { markAsReadAll } = useNotificationStore();
|
||||
|
||||
// Setup pagination
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page) => {
|
||||
if (!user?.id) return { data: [] };
|
||||
|
||||
return await apiGetNotificationsById({
|
||||
id: user?.id as any,
|
||||
category: activeCategory as any,
|
||||
page: String(page),
|
||||
});
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||
dependencies: [user?.id, activeCategory],
|
||||
onError: (error) =>
|
||||
console.error("[ERROR] Fetch admin notifications:", error),
|
||||
});
|
||||
|
||||
// Generate komponen
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
emptyMessage: "Belum ada notifikasi",
|
||||
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||
skeletonHeight: 100,
|
||||
});
|
||||
|
||||
// Render item notification
|
||||
const renderNotificationItem = ({ item }: { item: any }) => (
|
||||
<View key={item.id}>
|
||||
<BoxNotification
|
||||
data={item}
|
||||
activeCategory={activeCategory as any}
|
||||
setListData={pagination.setListData}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
|
||||
const handlePress = (item: any) => {
|
||||
setActiveCategory(item.value);
|
||||
// Reset pagination saat kategori berubah
|
||||
pagination.reset();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Admin Notifikasi",
|
||||
headerLeft: () => <BackButton />,
|
||||
headerRight: () => (
|
||||
<IconDot
|
||||
color={MainColor.yellow}
|
||||
onPress={() => setOpenDrawer(true)}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<NewWrapper
|
||||
headerComponent={
|
||||
<ScrollableCustom
|
||||
data={listOfcategoriesAppNotification.map((e, i) => ({
|
||||
id: i,
|
||||
label: e.label,
|
||||
value: e.value,
|
||||
}))}
|
||||
onButtonPress={handlePress}
|
||||
activeId={activeCategory as string}
|
||||
/>
|
||||
}
|
||||
listData={pagination.listData}
|
||||
renderItem={renderNotificationItem}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
tintColor={MainColor.yellow}
|
||||
colors={[MainColor.yellow]}
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
/>
|
||||
}
|
||||
onEndReached={pagination.loadMore}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
/>
|
||||
|
||||
<DrawerCustom
|
||||
isVisible={openDrawer}
|
||||
closeDrawer={() => setOpenDrawer(false)}
|
||||
height={"auto"}
|
||||
>
|
||||
<MenuDrawerDynamicGrid
|
||||
data={[
|
||||
{
|
||||
label: "Tandai Semua Dibaca",
|
||||
value: "read-all",
|
||||
icon: (
|
||||
<Ionicons
|
||||
name="reader-outline"
|
||||
size={ICON_SIZE_SMALL}
|
||||
color={MainColor.white}
|
||||
/>
|
||||
),
|
||||
path: "",
|
||||
},
|
||||
]}
|
||||
onPressItem={(item: any) => {
|
||||
console.log("Item", item.value);
|
||||
if (item.value === "read-all") {
|
||||
AlertDefaultSystem({
|
||||
title: "Tandai Semua Dibaca",
|
||||
message:
|
||||
"Apakah Anda yakin ingin menandai semua notifikasi dibaca?",
|
||||
textLeft: "Batal",
|
||||
textRight: "Ya",
|
||||
onPressRight: () => {
|
||||
markAsReadAll(user?.id as any);
|
||||
const data = _.cloneDeep(pagination.listData);
|
||||
data.forEach((e) => {
|
||||
e.isRead = true;
|
||||
});
|
||||
pagination.setListData(data);
|
||||
pagination.onRefresh();
|
||||
setOpenDrawer(false);
|
||||
},
|
||||
});
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</DrawerCustom>
|
||||
</>
|
||||
);
|
||||
}
|
||||
286
screens/Authentication/EULASection.tsx
Normal file
286
screens/Authentication/EULASection.tsx
Normal file
@@ -0,0 +1,286 @@
|
||||
// app/syarat-dan-ketentuan.tsx
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
ScrollView,
|
||||
TouchableOpacity,
|
||||
StyleSheet,
|
||||
} from "react-native";
|
||||
import { useState, useRef } from "react";
|
||||
import { router, useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
export default function EULASection({
|
||||
nomor,
|
||||
onSetModalVisible,
|
||||
setLoadingTerm,
|
||||
}: {
|
||||
nomor: string;
|
||||
onSetModalVisible: (visible: boolean) => void;
|
||||
setLoadingTerm: (loading: boolean) => void;
|
||||
}) {
|
||||
const { acceptedTerms } = useAuth();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const [isAtBottom, setIsAtBottom] = useState(false);
|
||||
const scrollViewRef = useRef<ScrollView>(null);
|
||||
|
||||
const handleScroll = (event: any) => {
|
||||
const { layoutMeasurement, contentOffset, contentSize } = event.nativeEvent;
|
||||
const paddingToBottom = 20;
|
||||
const isCloseToBottom =
|
||||
layoutMeasurement.height + contentOffset.y >=
|
||||
contentSize.height - paddingToBottom;
|
||||
setIsAtBottom(isCloseToBottom);
|
||||
};
|
||||
|
||||
const handleAccept = async () => {
|
||||
// console.log("Accept terms", nomor);
|
||||
// onSetModalVisible(false);
|
||||
try {
|
||||
if (!isAtBottom) return;
|
||||
|
||||
setIsLoading(true);
|
||||
const responseAccept = await acceptedTerms(
|
||||
nomor as string,
|
||||
onSetModalVisible,
|
||||
);
|
||||
|
||||
console.log("Accept terms", responseAccept);
|
||||
setLoadingTerm(true);
|
||||
|
||||
setTimeout(() => {
|
||||
router.replace(responseAccept);
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
console.log("Error accept terms", error);
|
||||
Toast.show({
|
||||
type: "error",
|
||||
text1: "Error",
|
||||
text2: "Terjadi kesalahan saat menerima syarat dan ketentuan",
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView edges={["bottom"]} style={styles.container}>
|
||||
<Text style={styles.title}>
|
||||
Syarat & Ketentuan Penggunaan HIPMI Badung Connect
|
||||
</Text>
|
||||
|
||||
<ScrollView
|
||||
ref={scrollViewRef}
|
||||
onScroll={handleScroll}
|
||||
scrollEventThrottle={16}
|
||||
style={styles.scrollView}
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
>
|
||||
<Text style={styles.paragraph}>
|
||||
Dengan menggunakan aplikasi{" "}
|
||||
<Text style={styles.bold}>HIPMI Badung Connect</Text> (“Aplikasi”),
|
||||
Anda setuju untuk mematuhi dan terikat oleh syarat dan ketentuan
|
||||
berikut. Jika Anda tidak setuju dengan ketentuan ini, harap jangan
|
||||
gunakan Aplikasi.
|
||||
</Text>
|
||||
|
||||
<Text style={styles.heading}>1. Definisi</Text>
|
||||
<Text style={styles.paragraph}>
|
||||
<Text style={styles.bold}>HIPMI Badung Connect</Text> adalah platform
|
||||
digital resmi untuk anggota Himpunan Pengusaha Muda Indonesia (HIPMI)
|
||||
Kabupaten Badung, yang bertujuan memfasilitasi jaringan, kolaborasi,
|
||||
dan pertumbuhan bisnis para pengusaha muda.
|
||||
</Text>
|
||||
|
||||
<Text style={styles.heading}>2. Larangan Konten Tidak Pantas</Text>
|
||||
<Text style={styles.paragraph}>
|
||||
Anda <Text style={styles.bold}>dilarang keras</Text> memposting,
|
||||
mengirim, membagikan, atau mengunggah konten apa pun yang mengandung:
|
||||
</Text>
|
||||
<View style={styles.list}>
|
||||
<Text style={styles.listItem}>
|
||||
• Ujaran kebencian, diskriminasi, atau konten SARA (Suku, Agama,
|
||||
Ras, Antar-golongan)
|
||||
</Text>
|
||||
<Text style={styles.listItem}>
|
||||
• Pornografi, konten seksual eksplisit, atau gambar tidak senonoh
|
||||
</Text>
|
||||
<Text style={styles.listItem}>
|
||||
• Ancaman, pelecehan, bullying, atau perilaku melecehkan
|
||||
</Text>
|
||||
<Text style={styles.listItem}>
|
||||
• Informasi palsu, hoaks, spam, atau konten menyesatkan
|
||||
</Text>
|
||||
<Text style={styles.listItem}>
|
||||
• Konten ilegal, melanggar hukum, atau melanggar hak kekayaan
|
||||
intelektual pihak lain
|
||||
</Text>
|
||||
<Text style={styles.listItem}>
|
||||
• Promosi narkoba, perjudian, atau aktivitas ilegal lainnya
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text style={styles.heading}>3. Tanggung Jawab Pengguna</Text>
|
||||
<Text style={styles.paragraph}>
|
||||
Anda bertanggung jawab penuh atas setiap konten yang Anda unggah atau
|
||||
bagikan melalui fitur-fitur berikut:
|
||||
</Text>
|
||||
<View style={styles.list}>
|
||||
<Text style={styles.listItem}>• Profil (bio, foto, portofolio)</Text>
|
||||
<Text style={styles.listItem}>• Forum diskusi</Text>
|
||||
<Text style={styles.listItem}>• Chat pribadi atau grup</Text>
|
||||
<Text style={styles.listItem}>
|
||||
• Lowongan kerja, investasi, dan donasi
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={styles.paragraph}>
|
||||
Konten yang melanggar ketentuan ini dapat dihapus kapan saja tanpa
|
||||
pemberitahuan.
|
||||
</Text>
|
||||
|
||||
<Text style={styles.heading}>4. Tindakan terhadap Pelanggaran</Text>
|
||||
<Text style={styles.paragraph}>
|
||||
Jika kami menerima laporan atau menemukan konten yang melanggar
|
||||
ketentuan ini, kami akan:
|
||||
</Text>
|
||||
<View style={styles.list}>
|
||||
<Text style={styles.listItem}>
|
||||
• Segera menghapus konten tersebut
|
||||
</Text>
|
||||
<Text style={styles.listItem}>
|
||||
• Memberikan peringatan atau memblokir akun pengguna
|
||||
</Text>
|
||||
<Text style={styles.listItem}>
|
||||
• Dalam kasus berat, melaporkan ke pihak berwajib sesuai hukum yang
|
||||
berlaku
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={styles.paragraph}>
|
||||
Tim kami berkomitmen untuk menanggapi laporan konten tidak pantas{" "}
|
||||
<Text style={styles.bold}>dalam waktu 24 jam</Text>.
|
||||
</Text>
|
||||
|
||||
<Text style={styles.heading}>5. Mekanisme Pelaporan</Text>
|
||||
<Text style={styles.paragraph}>
|
||||
Anda dapat melaporkan konten atau pengguna yang mencurigakan melalui:
|
||||
</Text>
|
||||
<View style={styles.list}>
|
||||
<Text style={styles.listItem}>
|
||||
• Tombol <Text style={styles.bold}>“Laporkan”</Text> di setiap
|
||||
posting forum atau pesan chat
|
||||
</Text>
|
||||
<Text style={styles.listItem}>
|
||||
• Tombol <Text style={styles.bold}>“Blokir Pengguna”</Text> di
|
||||
profil pengguna
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={styles.paragraph}>
|
||||
Setiap laporan akan ditangani secara rahasia dan segera.
|
||||
</Text>
|
||||
|
||||
<Text style={styles.heading}>6. Perubahan Ketentuan</Text>
|
||||
<Text style={styles.paragraph}>
|
||||
Kami berhak memperbarui Syarat & Ketentuan ini sewaktu-waktu. Versi
|
||||
terbaru akan dipublikasikan di halaman ini dengan tanggal revisi yang
|
||||
diperbarui.
|
||||
</Text>
|
||||
|
||||
<Text style={styles.heading}>7. Kontak</Text>
|
||||
<Text style={styles.paragraph}>
|
||||
Jika Anda memiliki pertanyaan tentang ketentuan ini, silakan hubungi
|
||||
kami di:{"\n"}
|
||||
<Text style={[styles.bold, { color: "#1E90FF" }]}>
|
||||
bip.baliinteraktifperkasa@gmail.com
|
||||
</Text>
|
||||
</Text>
|
||||
|
||||
<Text style={styles.footer}>
|
||||
© 2026 Bali Interaktif Perkasa. All rights reserved.
|
||||
</Text>
|
||||
</ScrollView>
|
||||
|
||||
<TouchableOpacity
|
||||
onPress={handleAccept}
|
||||
disabled={!isAtBottom || isLoading}
|
||||
style={[styles.button, { opacity: !isAtBottom || isLoading ? 0.6 : 1 }]}
|
||||
>
|
||||
<Text style={styles.buttonText}>
|
||||
{isLoading ? "Menyimpan..." : "Saya Setuju"}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: MainColor.darkblue,
|
||||
padding: 16,
|
||||
},
|
||||
title: {
|
||||
fontSize: 20,
|
||||
fontWeight: "bold",
|
||||
textAlign: "center",
|
||||
marginBottom: 16,
|
||||
color: MainColor.white,
|
||||
},
|
||||
scrollView: {
|
||||
flex: 1,
|
||||
marginBottom: 20,
|
||||
},
|
||||
scrollContent: {
|
||||
paddingBottom: 30,
|
||||
},
|
||||
heading: {
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
marginTop: 16,
|
||||
marginBottom: 8,
|
||||
color: MainColor.white,
|
||||
},
|
||||
paragraph: {
|
||||
fontSize: 14,
|
||||
lineHeight: 22,
|
||||
color: MainColor.white,
|
||||
marginBottom: 12,
|
||||
},
|
||||
bold: {
|
||||
fontWeight: "600",
|
||||
},
|
||||
list: {
|
||||
marginLeft: 8,
|
||||
marginBottom: 12,
|
||||
},
|
||||
listItem: {
|
||||
fontSize: 14,
|
||||
lineHeight: 22,
|
||||
color: MainColor.white,
|
||||
marginBottom: 6,
|
||||
},
|
||||
footer: {
|
||||
fontSize: 12,
|
||||
color: MainColor.white,
|
||||
textAlign: "center",
|
||||
marginTop: 20,
|
||||
paddingTop: 10,
|
||||
borderTopWidth: 2,
|
||||
borderTopColor: AccentColor.blue,
|
||||
},
|
||||
button: {
|
||||
backgroundColor: MainColor.yellow,
|
||||
paddingVertical: 14,
|
||||
borderRadius: 8,
|
||||
alignItems: "center",
|
||||
width: 200,
|
||||
alignSelf: "center",
|
||||
},
|
||||
buttonText: {
|
||||
color: "#fff",
|
||||
fontSize: 16,
|
||||
fontWeight: "600",
|
||||
},
|
||||
});
|
||||
@@ -12,25 +12,6 @@ import { SafeAreaView } from "react-native-safe-area-context";
|
||||
import { AccentColor, MainColor } from "@/constants/color-palet";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
|
||||
// Ganti dengan API call ke backend Anda
|
||||
// const acceptEula = async (): Promise<boolean> => {
|
||||
// try {
|
||||
// const response = await fetch("/api/user/update-eula", {
|
||||
// method: "PATCH",
|
||||
// headers: { "Content-Type": "application/json" },
|
||||
// credentials: "include",
|
||||
// body: JSON.stringify({
|
||||
// eulaAcceptedAt: new Date().toISOString(),
|
||||
// eulaVersion: "2026-01-v1", // sesuaikan versi Anda
|
||||
// }),
|
||||
// });
|
||||
// return response.ok;
|
||||
// } catch (error) {
|
||||
// console.error("Gagal menyimpan persetujuan EULA:", error);
|
||||
// return false;
|
||||
// }
|
||||
// };
|
||||
|
||||
export default function EULAView() {
|
||||
const { acceptedTerms } = useAuth();
|
||||
const { nomor } = useLocalSearchParams();
|
||||
@@ -52,7 +33,7 @@ export default function EULAView() {
|
||||
if (!isAtBottom) return;
|
||||
|
||||
setIsLoading(true);
|
||||
await acceptedTerms(nomor as string);
|
||||
// await acceptedTerms(nomor as string);
|
||||
} catch (error) {
|
||||
console.log("Error accept terms", error);
|
||||
} finally {
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
import { NewWrapper } from "@/components";
|
||||
import ButtonCustom from "@/components/Button/ButtonCustom";
|
||||
import ModalReactNative from "@/components/Modal/ModalReactNative";
|
||||
import Spacing from "@/components/_ShareComponent/Spacing";
|
||||
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { apiVersion } from "@/service/api-config";
|
||||
import { apiVersion, BASE_URL } from "@/service/api-config";
|
||||
import { GStyles } from "@/styles/global-styles";
|
||||
import { openBrowser } from "@/utils/openBrower";
|
||||
import versionBadge from "@/utils/viersionBadge";
|
||||
import VersionBadge from "@/utils/viersionBadge";
|
||||
import { Redirect } from "expo-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { RefreshControl, Text, View } from "react-native";
|
||||
import PhoneInput, { ICountry } from "react-native-international-phone-number";
|
||||
import Toast from "react-native-toast-message";
|
||||
import EULASection from "./EULASection";
|
||||
|
||||
export default function LoginView() {
|
||||
const url = BASE_URL;
|
||||
const [version, setVersion] = useState<string>("");
|
||||
const [selectedCountry, setSelectedCountry] = useState<null | ICountry>(null);
|
||||
const [inputValue, setInputValue] = useState<string>("");
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [refreshing, setRefreshing] = useState<boolean>(false);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [numberToEULA, setNumberToEULA] = useState<string>("");
|
||||
const [loadingTerm, setLoadingTerm] = useState<boolean>(false);
|
||||
|
||||
const { loginWithNomor, token, isAdmin, isUserActive } = useAuth();
|
||||
|
||||
@@ -87,7 +92,13 @@ export default function LoginView() {
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
await loginWithNomor(realNumber);
|
||||
const loginRes = await loginWithNomor(realNumber);
|
||||
|
||||
if (!loginRes) {
|
||||
setModalVisible(true);
|
||||
}
|
||||
|
||||
setNumberToEULA(realNumber);
|
||||
} catch (error) {
|
||||
console.log("Error login", error);
|
||||
Toast.show({
|
||||
@@ -98,30 +109,6 @@ export default function LoginView() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
// try {
|
||||
// setLoading(true);
|
||||
// // const response = await apiLogin({ nomor: realNumber });
|
||||
// const response = await loginWithNomor(realNumber);
|
||||
// console.log("[RESPONSE]", response);
|
||||
|
||||
// Toast.show({
|
||||
// type: "success",
|
||||
// text1: "Sukses",
|
||||
// text2: "Kode OTP berhasil dikirim",
|
||||
// });
|
||||
|
||||
// // router.navigate(`/verification?nomor=${realNumber}`);
|
||||
// } catch (error) {
|
||||
// console.log("Error login", error);
|
||||
// Toast.show({
|
||||
// type: "error",
|
||||
// text1: "Error",
|
||||
// text2: error as string,
|
||||
// });
|
||||
// } finally {
|
||||
// setLoading(false);
|
||||
// }
|
||||
}
|
||||
|
||||
if (token && token !== "" && !isUserActive) {
|
||||
@@ -182,14 +169,42 @@ export default function LoginView() {
|
||||
|
||||
<Spacing />
|
||||
|
||||
<ButtonCustom onPress={handleLogin} isLoading={loading}>
|
||||
<ButtonCustom
|
||||
onPress={handleLogin}
|
||||
disabled={loadingTerm}
|
||||
isLoading={loading || loadingTerm}
|
||||
>
|
||||
Login
|
||||
</ButtonCustom>
|
||||
<Spacing />
|
||||
{/* <ButtonCustom onPress={() => router.navigate("/(application)/coba")}>
|
||||
Coba
|
||||
</ButtonCustom> */}
|
||||
<Spacing height={50} />
|
||||
|
||||
<Text
|
||||
style={{ ...GStyles.textLabel, textAlign: "center", fontSize: 12 }}
|
||||
>
|
||||
Dengan menggunakan aplikasi ini, Anda telah menyetujui{" "}
|
||||
<Text
|
||||
style={{
|
||||
color: MainColor.yellow,
|
||||
textDecorationLine: "underline",
|
||||
}}
|
||||
onPress={() => {
|
||||
const toUrl = `${url}/terms-of-service.html`;
|
||||
openBrowser(toUrl);
|
||||
}}
|
||||
>
|
||||
Syarat & Ketentuan
|
||||
</Text>{" "}
|
||||
dan seluruh kebijakan privasi yang berlaku.
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<ModalReactNative isVisible={modalVisible}>
|
||||
<EULASection
|
||||
nomor={numberToEULA || ""}
|
||||
onSetModalVisible={setModalVisible}
|
||||
setLoadingTerm={setLoadingTerm}
|
||||
/>
|
||||
</ModalReactNative>
|
||||
</NewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
AvatarUsernameAndOtherComponent,
|
||||
BoxWithHeaderSection,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
} from "@/components";
|
||||
|
||||
@@ -31,7 +31,7 @@ export default function Event_ButtonStatusSection({
|
||||
type: "success",
|
||||
text1: response.message,
|
||||
});
|
||||
router.back();
|
||||
router.replace(`/event/(tabs)/status?status=draft`);
|
||||
} else {
|
||||
Toast.show({
|
||||
type: "info",
|
||||
@@ -65,7 +65,7 @@ export default function Event_ButtonStatusSection({
|
||||
type: "success",
|
||||
text1: response.message,
|
||||
});
|
||||
router.back();
|
||||
router.replace(`/event/(tabs)/status?status=review`);
|
||||
} else {
|
||||
Toast.show({
|
||||
type: "info",
|
||||
@@ -99,7 +99,7 @@ export default function Event_ButtonStatusSection({
|
||||
type: "success",
|
||||
text1: response.message,
|
||||
});
|
||||
router.back();
|
||||
router.replace(`/event/(tabs)/status?status=draft`);
|
||||
} else {
|
||||
Toast.show({
|
||||
type: "info",
|
||||
|
||||
75
screens/Event/ScreenBeranda.tsx
Normal file
75
screens/Event/ScreenBeranda.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import { TextCustom } from "@/components";
|
||||
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||
import FloatingButton from "@/components/Button/FloatingButton";
|
||||
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 Event_BoxPublishSection from "@/screens/Event/BoxPublishSection";
|
||||
import { apiEventGetAll } from "@/service/api-client/api-event";
|
||||
import { dateTimeView } from "@/utils/dateTimeView";
|
||||
import { router } from "expo-router";
|
||||
import { RefreshControl } from "react-native";
|
||||
|
||||
|
||||
export default function Event_ScreenBeranda() {
|
||||
// Setup pagination
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page) => {
|
||||
return await apiEventGetAll({
|
||||
category: "beranda",
|
||||
page: String(page),
|
||||
});
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||
dependencies: [],
|
||||
onError: (error) => console.error("[ERROR] Fetch event beranda:", error),
|
||||
});
|
||||
|
||||
// Generate komponen
|
||||
const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
emptyMessage: "Belum ada event",
|
||||
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||
skeletonHeight: 100,
|
||||
});
|
||||
|
||||
// Render item event
|
||||
const renderEventItem = ({ item }: { item: any }) => (
|
||||
<Event_BoxPublishSection
|
||||
key={item.id}
|
||||
href={`/event/${item.id}/publish`}
|
||||
data={item}
|
||||
rightComponentAvatar={
|
||||
<TextCustom>
|
||||
{dateTimeView({ date: item?.tanggal, withoutTime: true })}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
return (
|
||||
<NewWrapper
|
||||
listData={pagination.listData}
|
||||
renderItem={renderEventItem}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
tintColor={MainColor.yellow}
|
||||
colors={[MainColor.yellow]}
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
/>
|
||||
}
|
||||
onEndReached={pagination.loadMore}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
hideFooter
|
||||
floatingButton={
|
||||
<FloatingButton onPress={() => router.push("/event/create")} />
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
102
screens/Event/ScreenContribution.tsx
Normal file
102
screens/Event/ScreenContribution.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
AvatarUsernameAndOtherComponent,
|
||||
BoxWithHeaderSection,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextCustom
|
||||
} 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 { apiEventGetAll } from "@/service/api-client/api-event";
|
||||
import { dateTimeView } from "@/utils/dateTimeView";
|
||||
import { RefreshControl } from "react-native";
|
||||
|
||||
|
||||
export default function Event_ScreenContribution() {
|
||||
const { user } = useAuth();
|
||||
|
||||
// Setup pagination
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page) => {
|
||||
if (!user?.id) return { data: [] };
|
||||
|
||||
return await apiEventGetAll({
|
||||
category: "contribution",
|
||||
userId: user?.id,
|
||||
page: String(page),
|
||||
});
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||
dependencies: [user?.id],
|
||||
onError: (error) =>
|
||||
console.error("[ERROR] Fetch event contribution:", error),
|
||||
});
|
||||
|
||||
// Generate komponen
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
emptyMessage: "Belum ada kontribusi",
|
||||
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||
skeletonHeight: 100,
|
||||
});
|
||||
|
||||
// Render item event
|
||||
const renderEventItem = ({ item }: { item: any }) => (
|
||||
<BoxWithHeaderSection
|
||||
key={item?.id}
|
||||
href={`/event/${item?.Event?.id}/contribution`}
|
||||
>
|
||||
<StackCustom>
|
||||
<AvatarUsernameAndOtherComponent
|
||||
avatar={item?.Event?.Author?.Profile?.imageId}
|
||||
avatarHref={`/profile/${item?.Event?.Author?.Profile?.id}`}
|
||||
name={item?.Event?.Author?.username}
|
||||
rightComponent={
|
||||
<TextCustom truncate>
|
||||
{dateTimeView({
|
||||
date: item?.Event?.tanggal,
|
||||
withoutTime: true,
|
||||
})}
|
||||
</TextCustom>
|
||||
}
|
||||
/>
|
||||
|
||||
<TextCustom bold align="center" size="xlarge" truncate={2}>
|
||||
{item?.Event?.title}
|
||||
</TextCustom>
|
||||
<Spacing height={0} />
|
||||
</StackCustom>
|
||||
</BoxWithHeaderSection>
|
||||
);
|
||||
|
||||
// useEffect(() => {
|
||||
// pagination.onRefresh();
|
||||
// }, []);
|
||||
|
||||
return (
|
||||
<NewWrapper
|
||||
listData={pagination.listData}
|
||||
renderItem={renderEventItem}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
tintColor={MainColor.yellow}
|
||||
colors={[MainColor.yellow]}
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
/>
|
||||
}
|
||||
onEndReached={pagination.loadMore}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
hideFooter
|
||||
/>
|
||||
);
|
||||
}
|
||||
121
screens/Event/ScreenHistory.tsx
Normal file
121
screens/Event/ScreenHistory.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { ButtonCustom, Spacing, TextCustom } from "@/components";
|
||||
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||
import { AccentColor, 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 Event_BoxPublishSection from "@/screens/Event/BoxPublishSection";
|
||||
import { apiEventGetAll } from "@/service/api-client/api-event";
|
||||
import { dateTimeView } from "@/utils/dateTimeView";
|
||||
import _ from "lodash";
|
||||
import { useState } from "react";
|
||||
import { RefreshControl, View } from "react-native";
|
||||
|
||||
export default function Event_ScreenHistory() {
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>("all");
|
||||
const { user } = useAuth();
|
||||
|
||||
// Setup pagination
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page) => {
|
||||
return await apiEventGetAll({
|
||||
category: activeCategory === "all" ? "all-history" : "my-history",
|
||||
userId: user?.id,
|
||||
page: String(page),
|
||||
});
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||
dependencies: [user?.id, activeCategory],
|
||||
onError: (error) => console.error("[ERROR] Fetch event history:", error),
|
||||
});
|
||||
|
||||
// Generate komponen
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
emptyMessage: "Belum ada riwayat",
|
||||
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||
skeletonHeight: 100,
|
||||
});
|
||||
|
||||
// Render item event
|
||||
const renderEventItem = ({ item }: { item: any }) => (
|
||||
<Event_BoxPublishSection
|
||||
key={item && item?.id}
|
||||
data={item}
|
||||
rightComponentAvatar={
|
||||
<TextCustom>
|
||||
{dateTimeView({ date: item?.tanggal, withoutTime: true })}
|
||||
</TextCustom>
|
||||
}
|
||||
href={`/event/${item.id}/history`}
|
||||
/>
|
||||
);
|
||||
|
||||
const handlePress = (item: any) => {
|
||||
setActiveCategory(item);
|
||||
// Reset pagination saat kategori berubah
|
||||
pagination.reset();
|
||||
};
|
||||
|
||||
const headerComponent = (
|
||||
<View
|
||||
style={{
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
padding: 5,
|
||||
backgroundColor: MainColor.soft_darkblue,
|
||||
borderRadius: 50,
|
||||
width: "100%",
|
||||
}}
|
||||
>
|
||||
<ButtonCustom
|
||||
backgroundColor={
|
||||
activeCategory === "all" ? MainColor.yellow : AccentColor.blue
|
||||
}
|
||||
textColor={activeCategory === "all" ? MainColor.black : MainColor.white}
|
||||
style={{ width: "49%" }}
|
||||
onPress={() => handlePress("all")}
|
||||
>
|
||||
Semua Riwayat
|
||||
</ButtonCustom>
|
||||
<Spacing width={"2%"} />
|
||||
<ButtonCustom
|
||||
backgroundColor={
|
||||
activeCategory === "main" ? MainColor.yellow : AccentColor.blue
|
||||
}
|
||||
textColor={
|
||||
activeCategory === "main" ? MainColor.black : MainColor.white
|
||||
}
|
||||
style={{ width: "49%" }}
|
||||
onPress={() => handlePress("main")}
|
||||
>
|
||||
Riwayat Saya
|
||||
</ButtonCustom>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<NewWrapper
|
||||
headerComponent={headerComponent}
|
||||
listData={pagination.listData}
|
||||
renderItem={renderEventItem}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
tintColor={MainColor.yellow}
|
||||
colors={[MainColor.yellow]}
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
/>
|
||||
}
|
||||
onEndReached={pagination.loadMore}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
hideFooter
|
||||
/>
|
||||
);
|
||||
}
|
||||
121
screens/Event/ScreenListOfParticipants.tsx
Normal file
121
screens/Event/ScreenListOfParticipants.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
AvatarUsernameAndOtherComponent,
|
||||
BadgeCustom,
|
||||
BaseBox
|
||||
} 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 { usePagination } from "@/hooks/use-pagination";
|
||||
import {
|
||||
apiEventGetOne,
|
||||
apiEventListOfParticipants,
|
||||
} from "@/service/api-client/api-event";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
import { useEffect, useState } from "react";
|
||||
import { RefreshControl, View } from "react-native";
|
||||
|
||||
export default function Event_ScreenListOfParticipants() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const [startDate, setStartDate] = useState<Dayjs | undefined>();
|
||||
|
||||
// Setup pagination
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page) => {
|
||||
return await apiEventListOfParticipants({
|
||||
id: id as string,
|
||||
page: String(page),
|
||||
});
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||
dependencies: [id],
|
||||
onError: (error) =>
|
||||
console.error("[ERROR] Fetch event participants:", error),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
onLoadData();
|
||||
}, []);
|
||||
|
||||
// Fetch event data separately (not part of pagination)
|
||||
// useFocusEffect(() => {
|
||||
// onLoadData();
|
||||
// pagination.onRefresh();
|
||||
// });
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
const response = await apiEventGetOne({ id: id as string });
|
||||
if (response.success) {
|
||||
const date = dayjs(response.data.tanggal);
|
||||
setStartDate(date);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Generate komponen
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
emptyMessage: "Belum ada peserta",
|
||||
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||
skeletonHeight: 100,
|
||||
});
|
||||
|
||||
// Render item participant
|
||||
const renderParticipantItem = ({ item }: { item: any }) => (
|
||||
<BaseBox key={item.id}>
|
||||
<AvatarUsernameAndOtherComponent
|
||||
avatar={item?.User?.Profile?.imageId}
|
||||
name={item?.User?.username}
|
||||
avatarHref={`/profile/${item?.User?.Profile?.id}`}
|
||||
rightComponent={
|
||||
startDate && startDate.subtract(1, "hour").diff(dayjs()) < 0 ? (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<BadgeCustom color={item?.isPresent ? "green" : "red"}>
|
||||
{item?.isPresent ? "Hadir" : "Tidak Hadir"}
|
||||
</BadgeCustom>
|
||||
</View>
|
||||
) : (
|
||||
<View
|
||||
style={{
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<BadgeCustom color="gray">-</BadgeCustom>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</BaseBox>
|
||||
);
|
||||
|
||||
return (
|
||||
<NewWrapper
|
||||
listData={pagination.listData}
|
||||
renderItem={renderParticipantItem}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
tintColor={MainColor.yellow}
|
||||
colors={[MainColor.yellow]}
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
/>
|
||||
}
|
||||
onEndReached={pagination.loadMore}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
122
screens/Event/ScreenStatus.tsx
Normal file
122
screens/Event/ScreenStatus.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BoxWithHeaderSection,
|
||||
Grid,
|
||||
ScrollableCustom,
|
||||
StackCustom,
|
||||
TextCustom,
|
||||
} from "@/components";
|
||||
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||
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 { apiEventGetByStatus } from "@/service/api-client/api-event";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
import { RefreshControl, View } from "react-native";
|
||||
|
||||
export default function Event_ScreenStatus() {
|
||||
const { user } = useAuth();
|
||||
const { status } = useLocalSearchParams<{ status?: string }>();
|
||||
|
||||
const id = user?.id || "";
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>(
|
||||
status || "publish",
|
||||
);
|
||||
|
||||
// Setup pagination
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page) => {
|
||||
if (!id) return { data: [] };
|
||||
|
||||
return await apiEventGetByStatus({
|
||||
id: id!,
|
||||
status: activeCategory!,
|
||||
page: String(page),
|
||||
});
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||
dependencies: [id, activeCategory],
|
||||
onError: (error) => console.error("[ERROR] Fetch event by status:", error),
|
||||
});
|
||||
|
||||
// Generate komponen
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
emptyMessage: `Tidak ada data ${activeCategory}`,
|
||||
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||
skeletonHeight: 100,
|
||||
});
|
||||
|
||||
// Render item event
|
||||
const renderEventItem = ({ item }: { item: any }) => (
|
||||
<BoxWithHeaderSection
|
||||
key={item.id}
|
||||
href={`/event/${item.id}/${activeCategory}/detail-event`}
|
||||
>
|
||||
<StackCustom gap={"xs"}>
|
||||
<Grid>
|
||||
<Grid.Col span={8}>
|
||||
<TextCustom truncate bold>
|
||||
{item?.title}
|
||||
</TextCustom>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
|
||||
<TextCustom>
|
||||
{new Date(item?.tanggal).toLocaleDateString()}
|
||||
</TextCustom>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
|
||||
<TextCustom truncate={2}>{item?.deskripsi}</TextCustom>
|
||||
</StackCustom>
|
||||
</BoxWithHeaderSection>
|
||||
);
|
||||
|
||||
const handlePress = (item: any) => {
|
||||
setActiveCategory(item.value);
|
||||
// Reset pagination saat kategori berubah
|
||||
pagination.reset();
|
||||
};
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
pagination.onRefresh();
|
||||
}, [activeCategory])
|
||||
);
|
||||
|
||||
const tabsComponent = (
|
||||
<ScrollableCustom
|
||||
data={dummyMasterStatus.map((e, i) => ({
|
||||
id: i,
|
||||
label: e.label,
|
||||
value: e.value,
|
||||
}))}
|
||||
onButtonPress={handlePress}
|
||||
activeId={activeCategory as any}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<NewWrapper
|
||||
hideFooter
|
||||
headerComponent={<View style={{ paddingTop: 8 }}>{tabsComponent}</View>}
|
||||
listData={pagination.listData}
|
||||
renderItem={renderEventItem}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
/>
|
||||
}
|
||||
onEndReached={pagination.loadMore}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
/>
|
||||
);
|
||||
}
|
||||
276
screens/Forum/DetailForum.tsx
Normal file
276
screens/Forum/DetailForum.tsx
Normal file
@@ -0,0 +1,276 @@
|
||||
import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCustom,
|
||||
DrawerCustom,
|
||||
LoaderCustom,
|
||||
NewWrapper,
|
||||
Spacing,
|
||||
TextAreaCustom,
|
||||
TextCustom,
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||
import AlertWarning from "@/components/Alert/AlertWarning";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import Forum_CommentarBoxSection from "@/screens/Forum/CommentarBoxSection";
|
||||
import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection";
|
||||
import Forum_MenuDrawerBerandaSection from "@/screens/Forum/MenuDrawerSection.tsx/MenuBeranda";
|
||||
import Forum_MenuDrawerCommentar from "@/screens/Forum/MenuDrawerSection.tsx/MenuCommentar";
|
||||
import {
|
||||
apiForumCreateComment,
|
||||
apiForumGetComment,
|
||||
apiForumGetOne,
|
||||
apiForumUpdateStatus,
|
||||
} from "@/service/api-client/api-forum";
|
||||
import { TypeForum_CommentProps } from "@/types/type-forum";
|
||||
import { isBadContent } from "@/utils/badWordsIndonesia";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
export default function DetailForum() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const { user } = useAuth();
|
||||
const [openDrawer, setOpenDrawer] = useState(false);
|
||||
const [data, setData] = useState<any | null>(null);
|
||||
const [listComment, setListComment] = useState<
|
||||
TypeForum_CommentProps[] | null
|
||||
>(null);
|
||||
const [isLoadingComment, setLoadingComment] = useState(false);
|
||||
|
||||
// Status
|
||||
const [status, setStatus] = useState("");
|
||||
const [text, setText] = useState("");
|
||||
const [authorId, setAuthorId] = useState("");
|
||||
const [dataId, setDataId] = useState("");
|
||||
|
||||
// Comentar
|
||||
const [openDrawerCommentar, setOpenDrawerCommentar] = useState(false);
|
||||
const [commentId, setCommentId] = useState("");
|
||||
const [commentAuthorId, setCommentAuthorId] = useState("");
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
setTimeout(() => {
|
||||
onLoadData(id as string);
|
||||
}, 3000);
|
||||
}, [id]),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
onLoadListComment(id as string);
|
||||
}, 3000);
|
||||
}, [id]);
|
||||
|
||||
const onLoadData = async (id: string) => {
|
||||
try {
|
||||
const response = await apiForumGetOne({ id });
|
||||
setData(response.data);
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
}
|
||||
};
|
||||
|
||||
const onLoadListComment = async (id: string) => {
|
||||
try {
|
||||
const response = await apiForumGetComment({
|
||||
id: id as string,
|
||||
});
|
||||
setListComment(response.data);
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Update Status
|
||||
const handlerUpdateStatus = async (value: any) => {
|
||||
try {
|
||||
const response = await apiForumUpdateStatus({
|
||||
id: id as string,
|
||||
data: value,
|
||||
});
|
||||
if (response.success) {
|
||||
setStatus(response.data);
|
||||
setData({
|
||||
...data,
|
||||
ForumMaster_StatusPosting: {
|
||||
status: response.data,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Create Commentar
|
||||
const handlerCreateCommentar = async () => {
|
||||
if (isBadContent(text)) {
|
||||
AlertWarning({});
|
||||
return;
|
||||
}
|
||||
|
||||
const newData = {
|
||||
comment: text,
|
||||
authorId: user?.id,
|
||||
};
|
||||
|
||||
try {
|
||||
setLoadingComment(true);
|
||||
const response = await apiForumCreateComment({
|
||||
id: id as string,
|
||||
data: newData,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setText("");
|
||||
const newComment = {
|
||||
id: response.data.id,
|
||||
isActive: response.data.isActive,
|
||||
komentar: response.data.komentar,
|
||||
createdAt: response.data.createdAt,
|
||||
authorId: response.data.authorId,
|
||||
Author: response.data.Author,
|
||||
};
|
||||
setListComment((prev) => [newComment, ...(prev || [])]);
|
||||
setData({
|
||||
...data,
|
||||
count: data.count + 1,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setLoadingComment(false);
|
||||
}
|
||||
};
|
||||
|
||||
const headerComponent = () =>
|
||||
// Box Posting
|
||||
!data && !listComment ? (
|
||||
<CustomSkeleton height={200} />
|
||||
) : (
|
||||
<>
|
||||
<Forum_BoxDetailSection
|
||||
data={data}
|
||||
onSetData={() => {
|
||||
setOpenDrawer(true);
|
||||
setStatus(data.ForumMaster_StatusPosting?.status);
|
||||
setAuthorId(data.Author?.id);
|
||||
setDataId(data.id);
|
||||
}}
|
||||
/>
|
||||
|
||||
{data?.ForumMaster_StatusPosting?.status === "Open" && (
|
||||
<>
|
||||
<TextAreaCustom
|
||||
placeholder="Ketik diskusi anda..."
|
||||
maxLength={1000}
|
||||
showCount
|
||||
value={text}
|
||||
onChangeText={setText}
|
||||
style={{
|
||||
marginBottom: 0,
|
||||
}}
|
||||
/>
|
||||
<ButtonCustom
|
||||
isLoading={isLoadingComment}
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
}}
|
||||
onPress={() => {
|
||||
handlerCreateCommentar();
|
||||
}}
|
||||
>
|
||||
Balas
|
||||
</ButtonCustom>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<NewWrapper
|
||||
// headerComponent={headerComponent()}
|
||||
>
|
||||
{!data && !listComment ? (
|
||||
<LoaderCustom />
|
||||
) : (
|
||||
<>
|
||||
{/* Area Commentar */}
|
||||
{headerComponent()}
|
||||
<Spacing height={40} />
|
||||
{/* List Commentar */}
|
||||
{_.isEmpty(listComment) ? (
|
||||
<TextCustom align="center" color="gray" size={"small"}>
|
||||
Tidak ada komentar
|
||||
</TextCustom>
|
||||
) : (
|
||||
<TextCustom color="gray">Komentar :</TextCustom>
|
||||
)}
|
||||
<Spacing height={5} />
|
||||
{listComment?.map((item: any, index: number) => (
|
||||
<Forum_CommentarBoxSection
|
||||
key={index}
|
||||
data={item}
|
||||
onSetData={(value) => {
|
||||
setCommentId(value.setCommentId);
|
||||
setOpenDrawerCommentar(value.setOpenDrawer);
|
||||
setCommentAuthorId(value.setCommentAuthorId);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</NewWrapper>
|
||||
|
||||
{/* Posting Drawer */}
|
||||
<DrawerCustom
|
||||
height={"auto"}
|
||||
isVisible={openDrawer}
|
||||
closeDrawer={() => setOpenDrawer(false)}
|
||||
>
|
||||
<Forum_MenuDrawerBerandaSection
|
||||
id={dataId}
|
||||
authorUsername={data?.Author?.username as string}
|
||||
status={status}
|
||||
setIsDrawerOpen={() => {
|
||||
setOpenDrawer(false);
|
||||
}}
|
||||
authorId={authorId}
|
||||
handlerUpdateStatus={(value: any) => {
|
||||
handlerUpdateStatus(value);
|
||||
}}
|
||||
/>
|
||||
</DrawerCustom>
|
||||
|
||||
{/* Commentar Drawer */}
|
||||
<DrawerCustom
|
||||
height={"auto"}
|
||||
isVisible={openDrawerCommentar}
|
||||
closeDrawer={() => setOpenDrawerCommentar(false)}
|
||||
>
|
||||
<Forum_MenuDrawerCommentar
|
||||
id={commentId as string}
|
||||
commentId={commentId}
|
||||
commentAuthorId={commentAuthorId}
|
||||
setIsDrawerOpen={() => {
|
||||
setOpenDrawerCommentar(false);
|
||||
}}
|
||||
listComment={listComment}
|
||||
setListComment={setListComment}
|
||||
countComment={data?.count}
|
||||
setCountComment={(val: any) => {
|
||||
setData((prev: any) => ({
|
||||
...prev,
|
||||
count: val,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</DrawerCustom>
|
||||
</>
|
||||
);
|
||||
}
|
||||
320
screens/Forum/DetailForum2.tsx
Normal file
320
screens/Forum/DetailForum2.tsx
Normal file
@@ -0,0 +1,320 @@
|
||||
import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCustom,
|
||||
DrawerCustom,
|
||||
LoaderCustom,
|
||||
NewWrapper,
|
||||
Spacing,
|
||||
StackCustom,
|
||||
TextAreaCustom,
|
||||
TextCustom,
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||
import AlertWarning from "@/components/Alert/AlertWarning";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { usePagination } from "@/hooks/use-pagination";
|
||||
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||
import Forum_CommentarBoxSection from "@/screens/Forum/CommentarBoxSection";
|
||||
import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection";
|
||||
import Forum_MenuDrawerBerandaSection from "@/screens/Forum/MenuDrawerSection.tsx/MenuBeranda";
|
||||
import Forum_MenuDrawerCommentar from "@/screens/Forum/MenuDrawerSection.tsx/MenuCommentar";
|
||||
import {
|
||||
apiForumCreateComment,
|
||||
apiForumGetComment,
|
||||
apiForumGetOne,
|
||||
apiForumUpdateStatus,
|
||||
} from "@/service/api-client/api-forum";
|
||||
import { TypeForum_CommentProps } from "@/types/type-forum";
|
||||
import { censorText, isBadContent } from "@/utils/badWordsIndonesia";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { RefreshControl } from "react-native";
|
||||
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
|
||||
|
||||
export default function DetailForum2() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const { user } = useAuth();
|
||||
const [openDrawer, setOpenDrawer] = useState(false);
|
||||
const [data, setData] = useState<any | null>(null);
|
||||
const [isLoadingComment, setLoadingComment] = useState(false);
|
||||
|
||||
// Status
|
||||
const [status, setStatus] = useState("");
|
||||
const [text, setText] = useState("");
|
||||
const [authorId, setAuthorId] = useState("");
|
||||
const [dataId, setDataId] = useState("");
|
||||
|
||||
// Comentar
|
||||
const [openDrawerCommentar, setOpenDrawerCommentar] = useState(false);
|
||||
const [commentId, setCommentId] = useState("");
|
||||
const [commentAuthorId, setCommentAuthorId] = useState("");
|
||||
|
||||
// Initialize pagination for comments
|
||||
const commentPagination = usePagination({
|
||||
fetchFunction: async (page) => {
|
||||
return await apiForumGetComment({
|
||||
id: id as string,
|
||||
page: String(page), // API expects string
|
||||
});
|
||||
},
|
||||
pageSize: 5,
|
||||
dependencies: [id],
|
||||
onError: (error) => console.error("[ERROR] Fetch forum comment:", error),
|
||||
});
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
setTimeout(() => {
|
||||
onLoadData(id as string);
|
||||
}, 3000);
|
||||
}, [id]),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Reset and load first page of comments when id changes
|
||||
commentPagination.reset();
|
||||
commentPagination.onRefresh();
|
||||
}, [id]);
|
||||
|
||||
const onLoadData = async (id: string) => {
|
||||
try {
|
||||
const response = await apiForumGetOne({ id });
|
||||
setData(response.data);
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Update Status
|
||||
const handlerUpdateStatus = async (value: any) => {
|
||||
try {
|
||||
const response = await apiForumUpdateStatus({
|
||||
id: id as string,
|
||||
data: value,
|
||||
});
|
||||
if (response.success) {
|
||||
setStatus(response.data);
|
||||
setData({
|
||||
...data,
|
||||
ForumMaster_StatusPosting: {
|
||||
status: response.data,
|
||||
},
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Create Commentar
|
||||
const handlerCreateCommentar = async () => {
|
||||
const cencorContent = censorText(text);
|
||||
|
||||
const newData = {
|
||||
comment: cencorContent,
|
||||
authorId: user?.id,
|
||||
};
|
||||
|
||||
try {
|
||||
setLoadingComment(true);
|
||||
const response = await apiForumCreateComment({
|
||||
id: id as string,
|
||||
data: newData,
|
||||
});
|
||||
|
||||
if (response.success) {
|
||||
setText("");
|
||||
const newComment = {
|
||||
id: response.data.id,
|
||||
isActive: response.data.isActive,
|
||||
komentar: response.data.komentar,
|
||||
createdAt: response.data.createdAt,
|
||||
authorId: response.data.authorId,
|
||||
Author: response.data.Author,
|
||||
};
|
||||
|
||||
// Add new comment to the top of the list
|
||||
commentPagination.setListData((prev) => [newComment, ...prev]);
|
||||
|
||||
setData({
|
||||
...data,
|
||||
count: data.count + 1,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setLoadingComment(false);
|
||||
}
|
||||
};
|
||||
|
||||
const headerComponent = () =>
|
||||
// Box Posting
|
||||
!data ? (
|
||||
<StackCustom>
|
||||
<CustomSkeleton height={200} />
|
||||
<CustomSkeleton height={100} />
|
||||
</StackCustom>
|
||||
) : (
|
||||
<>
|
||||
{/* Area Posting */}
|
||||
<Forum_BoxDetailSection
|
||||
data={data}
|
||||
onSetData={() => {
|
||||
setOpenDrawer(true);
|
||||
setStatus(data.ForumMaster_StatusPosting?.status);
|
||||
setAuthorId(data.Author?.id);
|
||||
setDataId(data.id);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Area Commentar */}
|
||||
{data?.ForumMaster_StatusPosting?.status === "Open" && (
|
||||
<>
|
||||
<TextAreaCustom
|
||||
placeholder="Ketik diskusi anda..."
|
||||
maxLength={1000}
|
||||
showCount
|
||||
value={text}
|
||||
onChangeText={setText}
|
||||
style={{
|
||||
marginBottom: 0,
|
||||
}}
|
||||
/>
|
||||
<ButtonCustom
|
||||
isLoading={isLoadingComment}
|
||||
style={{
|
||||
alignSelf: "flex-end",
|
||||
}}
|
||||
onPress={() => {
|
||||
handlerCreateCommentar();
|
||||
}}
|
||||
>
|
||||
Balas
|
||||
</ButtonCustom>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
// Render individual comment item
|
||||
const renderCommentItem = ({ item }: { item: TypeForum_CommentProps }) =>(
|
||||
<Forum_CommentarBoxSection
|
||||
key={item.id}
|
||||
data={item}
|
||||
onSetData={(value) => {
|
||||
setCommentId(value.setCommentId);
|
||||
setOpenDrawerCommentar(value.setOpenDrawer);
|
||||
setCommentAuthorId(value.setCommentAuthorId);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
// !data || !commentPagination.listData ? (
|
||||
// // <ListSkeletonComponent height={120} />
|
||||
// <LoaderCustom />
|
||||
// ) : (
|
||||
// <Forum_CommentarBoxSection
|
||||
// key={item.id}
|
||||
// data={item}
|
||||
// onSetData={(value) => {
|
||||
// setCommentId(value.setCommentId);
|
||||
// setOpenDrawerCommentar(value.setOpenDrawer);
|
||||
// setCommentAuthorId(value.setCommentAuthorId);
|
||||
// }}
|
||||
// />
|
||||
// );
|
||||
|
||||
// Generate pagination components using helper
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
createPaginationComponents({
|
||||
loading: commentPagination.loading,
|
||||
refreshing: commentPagination.refreshing,
|
||||
listData: commentPagination.listData,
|
||||
isInitialLoad: commentPagination.isInitialLoad,
|
||||
emptyMessage: "Tidak ada komentar",
|
||||
skeletonCount: 3,
|
||||
skeletonHeight: 120,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<NewWrapper
|
||||
// headerComponent={}
|
||||
listData={commentPagination.listData}
|
||||
renderItem={renderCommentItem}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
// IOS
|
||||
tintColor={MainColor.yellow}
|
||||
// Android
|
||||
colors={[MainColor.yellow]}
|
||||
progressBackgroundColor={MainColor.yellow}
|
||||
refreshing={commentPagination.refreshing}
|
||||
onRefresh={commentPagination.onRefresh}
|
||||
/>
|
||||
}
|
||||
onEndReached={commentPagination.loadMore}
|
||||
ListHeaderComponent={
|
||||
<>
|
||||
{/* <Spacing height={40} />
|
||||
<TextCustom color="gray">Komentar :</TextCustom>
|
||||
<Spacing height={5} /> */}
|
||||
{headerComponent()}
|
||||
<Spacing />
|
||||
</>
|
||||
}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
/>
|
||||
|
||||
{/* Posting Drawer */}
|
||||
<DrawerCustom
|
||||
height={"auto"}
|
||||
isVisible={openDrawer}
|
||||
closeDrawer={() => setOpenDrawer(false)}
|
||||
>
|
||||
<Forum_MenuDrawerBerandaSection
|
||||
id={dataId}
|
||||
authorUsername={data?.Author?.username as string}
|
||||
status={status}
|
||||
setIsDrawerOpen={() => {
|
||||
setOpenDrawer(false);
|
||||
}}
|
||||
authorId={authorId}
|
||||
handlerUpdateStatus={(value: any) => {
|
||||
handlerUpdateStatus(value);
|
||||
}}
|
||||
/>
|
||||
</DrawerCustom>
|
||||
|
||||
{/* Commentar Drawer */}
|
||||
<DrawerCustom
|
||||
height={"auto"}
|
||||
isVisible={openDrawerCommentar}
|
||||
closeDrawer={() => setOpenDrawerCommentar(false)}
|
||||
>
|
||||
<Forum_MenuDrawerCommentar
|
||||
id={commentId as string}
|
||||
commentId={commentId}
|
||||
commentAuthorId={commentAuthorId}
|
||||
setIsDrawerOpen={() => {
|
||||
setOpenDrawerCommentar(false);
|
||||
}}
|
||||
listComment={commentPagination.listData}
|
||||
setListComment={commentPagination.setListData}
|
||||
countComment={data?.count}
|
||||
setCountComment={(val: any) => {
|
||||
setData((prev: any) => ({
|
||||
...prev,
|
||||
count: val,
|
||||
}));
|
||||
}}
|
||||
/>
|
||||
</DrawerCustom>
|
||||
</>
|
||||
);
|
||||
}
|
||||
137
screens/Forum/ViewBeranda3.tsx
Normal file
137
screens/Forum/ViewBeranda3.tsx
Normal file
@@ -0,0 +1,137 @@
|
||||
import {
|
||||
AvatarComp,
|
||||
BackButton,
|
||||
FloatingButton,
|
||||
SearchInput,
|
||||
} from "@/components";
|
||||
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { usePagination } from "@/hooks/use-pagination";
|
||||
import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection";
|
||||
import { apiForumGetAll } from "@/service/api-client/api-forum";
|
||||
import { apiUser } from "@/service/api-client/api-user";
|
||||
import { router, Stack } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useEffect, useState } from "react";
|
||||
import { RefreshControl, TouchableOpacity, View } from "react-native";
|
||||
|
||||
const PAGE_SIZE = 5;
|
||||
|
||||
export default function Forum_ViewBeranda3() {
|
||||
const { user } = useAuth();
|
||||
const [dataUser, setDataUser] = useState<any>(null);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
// Load data profil user
|
||||
useEffect(() => {
|
||||
if (user?.id) {
|
||||
apiUser(user.id).then((res) => setDataUser(res.data));
|
||||
}
|
||||
}, [user?.id]);
|
||||
|
||||
// Setup pagination (menggantikan 50+ lines code!)
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page, searchQuery) => {
|
||||
if (!user?.id) return { data: [] };
|
||||
|
||||
return await apiForumGetAll({
|
||||
category: "beranda",
|
||||
search: searchQuery || "",
|
||||
userLoginId: user.id,
|
||||
page: String(page),
|
||||
});
|
||||
},
|
||||
pageSize: PAGE_SIZE,
|
||||
searchQuery: search,
|
||||
dependencies: [user?.id],
|
||||
onError: (error) => console.error("[ERROR] Fetch forum:", error),
|
||||
});
|
||||
|
||||
// Generate komponen (menggantikan 40+ lines code!)
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
searchQuery: search,
|
||||
emptyMessage: "Tidak ada diskusi",
|
||||
emptySearchMessage: "Tidak ada hasil pencarian",
|
||||
skeletonCount: 5,
|
||||
skeletonHeight: 150,
|
||||
});
|
||||
|
||||
// Render item forum
|
||||
const renderForumItem = ({ item }: { item: any }) => (
|
||||
<Forum_BoxDetailSection
|
||||
key={item.id}
|
||||
data={item}
|
||||
onSetData={() => {}}
|
||||
isTruncate={true}
|
||||
href={`/forum/${item.id}`}
|
||||
isRightComponent={false}
|
||||
/>
|
||||
);
|
||||
|
||||
// const ListHeaderComponent = (
|
||||
// <View style={{ paddingVertical: 8, alignItems: "center" }}>
|
||||
// <TextCustom>Diskusi Terbaru</TextCustom>
|
||||
// </View>
|
||||
// );
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack.Screen
|
||||
options={{
|
||||
title: "Forum",
|
||||
headerLeft: () => <BackButton />,
|
||||
headerRight: () => (
|
||||
<TouchableOpacity
|
||||
onPress={() => router.navigate(`/forum/${user?.id}/forumku`)}
|
||||
>
|
||||
<AvatarComp
|
||||
fileId={dataUser?.Profile?.imageId}
|
||||
size="base"
|
||||
href={`/forum/${user?.id}/forumku`}
|
||||
/>
|
||||
</TouchableOpacity>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
|
||||
<NewWrapper
|
||||
hideFooter
|
||||
headerComponent={
|
||||
<View style={{ paddingTop: 8 }}>
|
||||
<SearchInput
|
||||
placeholder="Cari topik diskusi"
|
||||
onChangeText={_.debounce((text) => setSearch(text), 500)}
|
||||
/>
|
||||
</View>
|
||||
}
|
||||
floatingButton={
|
||||
<FloatingButton
|
||||
onPress={() =>
|
||||
router.navigate("/(application)/(user)/forum/create")
|
||||
}
|
||||
/>
|
||||
}
|
||||
listData={pagination.listData}
|
||||
renderItem={renderForumItem}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
tintColor={MainColor.yellow}
|
||||
colors={[MainColor.yellow]}
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
/>
|
||||
}
|
||||
onEndReached={pagination.loadMore}
|
||||
// ListHeaderComponent={ListHeaderComponent}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -15,38 +15,49 @@ import NoDataText from "@/components/_ShareComponent/NoDataText";
|
||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { usePagination } from "@/hooks/use-pagination";
|
||||
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||
import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection";
|
||||
import { apiForumGetAll } from "@/service/api-client/api-forum";
|
||||
import { apiUser } from "@/service/api-client/api-user";
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { RefreshControl, View } from "react-native";
|
||||
|
||||
const PAGE_SIZE = 5;
|
||||
|
||||
export default function View_Forumku2() {
|
||||
const { id } = useLocalSearchParams();
|
||||
const { user } = useAuth();
|
||||
const [listData, setListData] = useState<any[]>([]);
|
||||
const [dataUser, setDataUser] = useState<any | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const [page, setPage] = useState(1);
|
||||
const [count, setCount] = useState(0);
|
||||
|
||||
// Initialize pagination hook
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page) => {
|
||||
if (!user?.id) throw new Error("User not authenticated");
|
||||
|
||||
const response = await apiForumGetAll({
|
||||
category: "forumku",
|
||||
authorId: id as string,
|
||||
userLoginId: user.id,
|
||||
page: String(page), // API terima string
|
||||
});
|
||||
|
||||
// Update count when fetching page 1
|
||||
if (page === 1) {
|
||||
setCount(response.data.count);
|
||||
}
|
||||
|
||||
return response.data;
|
||||
},
|
||||
pageSize: 5,
|
||||
dependencies: [user?.id],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
onLoadDataProfile(id as string);
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
setPage(1);
|
||||
setListData([]);
|
||||
setHasMore(true);
|
||||
fetchData(1, true);
|
||||
}, [user?.id]);
|
||||
|
||||
const onLoadDataProfile = async (id: string) => {
|
||||
try {
|
||||
const response = await apiUser(id);
|
||||
@@ -58,54 +69,6 @@ export default function View_Forumku2() {
|
||||
}
|
||||
};
|
||||
|
||||
// 🔹 Fungsi fetch data
|
||||
const fetchData = async (pageNumber: number, clear: boolean) => {
|
||||
if (!user?.id) return;
|
||||
|
||||
// Cegah multiple call
|
||||
if (!clear && (loading || refreshing)) return;
|
||||
|
||||
const isRefresh = clear;
|
||||
if (isRefresh) setRefreshing(true);
|
||||
if (!isRefresh) setLoading(true);
|
||||
|
||||
try {
|
||||
const response = await apiForumGetAll({
|
||||
category: "forumku",
|
||||
authorId: id as string,
|
||||
userLoginId: user.id,
|
||||
page: String(pageNumber), // API terima string
|
||||
});
|
||||
|
||||
const newData = response.data.data || [];
|
||||
setListData((prev) => {
|
||||
const current = Array.isArray(prev) ? prev : [];
|
||||
return clear ? newData : [...current, ...newData];
|
||||
});
|
||||
setHasMore(newData.length === PAGE_SIZE);
|
||||
setPage(pageNumber);
|
||||
setCount(response.data.count);
|
||||
} catch (error) {
|
||||
console.error("[ERROR] Fetch forum:", error);
|
||||
setHasMore(false);
|
||||
} finally {
|
||||
setRefreshing(false);
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 🔹 Pull-to-refresh
|
||||
const onRefresh = useCallback(() => {
|
||||
fetchData(1, true);
|
||||
}, [user?.id]);
|
||||
|
||||
// 🔹 Infinite scroll
|
||||
const loadMore = useCallback(() => {
|
||||
if (hasMore && !loading && !refreshing) {
|
||||
fetchData(page + 1, false);
|
||||
}
|
||||
}, [hasMore, loading, refreshing, page, user?.id]);
|
||||
|
||||
const randerHeaderComponent = () => (
|
||||
<>
|
||||
<CenterCustom>
|
||||
@@ -144,39 +107,16 @@ export default function View_Forumku2() {
|
||||
/>
|
||||
);
|
||||
|
||||
// Skeleton List (untuk initial load)
|
||||
const SkeletonListComponent = () => (
|
||||
<View style={{ flex: 1 }}>
|
||||
<StackCustom>
|
||||
{Array.from({ length: 5 }).map((_, i) => (
|
||||
<CustomSkeleton height={200} key={i} />
|
||||
))}
|
||||
</StackCustom>
|
||||
</View>
|
||||
);
|
||||
|
||||
// Komponen Empty
|
||||
const EmptyComponent = () => (
|
||||
<View
|
||||
style={{
|
||||
flex: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
padding: 20,
|
||||
}}
|
||||
>
|
||||
<NoDataText />
|
||||
</View>
|
||||
);
|
||||
|
||||
// 🔹 Komponen Footer List (loading indicator)
|
||||
const ListFooterComponent =
|
||||
loading && !refreshing && listData.length > 0 ? (
|
||||
<View style={{ paddingVertical: 16, alignItems: "center" }}>
|
||||
{/* <Text style={{ color: "#aaa", fontSize: 14 }}>Memuat diskusi...</Text> */}
|
||||
<LoaderCustom />
|
||||
</View>
|
||||
) : null;
|
||||
// Generate pagination components using helper
|
||||
const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
isInitialLoad: pagination.isInitialLoad,
|
||||
emptyMessage: "Tidak ada postingan",
|
||||
skeletonCount: 5,
|
||||
skeletonHeight: 200,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -190,7 +130,7 @@ export default function View_Forumku2() {
|
||||
/>
|
||||
)
|
||||
}
|
||||
listData={listData}
|
||||
listData={pagination.listData}
|
||||
renderItem={renderList}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
@@ -199,16 +139,14 @@ export default function View_Forumku2() {
|
||||
// Android
|
||||
colors={[MainColor.yellow]}
|
||||
progressBackgroundColor={MainColor.yellow}
|
||||
refreshing={refreshing}
|
||||
onRefresh={onRefresh}
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
/>
|
||||
}
|
||||
onEndReached={loadMore}
|
||||
onEndReached={pagination.loadMore}
|
||||
ListHeaderComponent={randerHeaderComponent()}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
ListEmptyComponent={
|
||||
loading && _.isEmpty(listData) ? <SkeletonListComponent /> : <EmptyComponent />
|
||||
}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -13,7 +13,8 @@ export const tabsHome: any = ({
|
||||
icon: "chatbubble-ellipses-outline",
|
||||
activeIcon: "chatbubble-ellipses",
|
||||
label: "Forum",
|
||||
path: acceptedForumTermsAt ? "/forum" : "/forum/terms",
|
||||
// path: acceptedForumTermsAt ? "/forum" : "/forum/terms",
|
||||
path: "/forum",
|
||||
isActive: true,
|
||||
disabled: false,
|
||||
},
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
TextCustom,
|
||||
} from "@/components";
|
||||
import API_STRORAGE from "@/constants/base-url-api-strorage";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import DUMMY_IMAGE from "@/constants/dummy-image-value";
|
||||
import { countDownAndCondition } from "@/utils/countDownAndCondition";
|
||||
import { Ionicons } from "@expo/vector-icons";
|
||||
@@ -22,8 +21,6 @@ export default function Investment_BoxBerandaSection({
|
||||
id: string;
|
||||
data: any;
|
||||
}) {
|
||||
// console.log("[DATA By one]", JSON.stringify(data, null, 2));
|
||||
|
||||
const [value, setValue] = useState({
|
||||
sisa: 0,
|
||||
reminder: false,
|
||||
@@ -33,8 +30,6 @@ export default function Investment_BoxBerandaSection({
|
||||
updateCountDown();
|
||||
}, [data]);
|
||||
|
||||
console.log("[DATA BERANDA]", JSON.stringify(data, null, 2));
|
||||
|
||||
const updateCountDown = () => {
|
||||
const countDown = countDownAndCondition({
|
||||
duration: data?.pencarianInvestor,
|
||||
|
||||
@@ -20,6 +20,9 @@ export default function Job_ButtonStatusSection({
|
||||
onSetLoading: (value: boolean) => void;
|
||||
isArchive?: boolean;
|
||||
}) {
|
||||
const path : any =(status: string) => {
|
||||
return `/job/(tabs)/status?status=${status}`
|
||||
}
|
||||
const handleBatalkanReview = () => {
|
||||
AlertDefaultSystem({
|
||||
title: "Batalkan Review",
|
||||
@@ -39,7 +42,7 @@ export default function Job_ButtonStatusSection({
|
||||
type: "success",
|
||||
text1: response.message,
|
||||
});
|
||||
router.back();
|
||||
router.replace(path("draft"));
|
||||
} else {
|
||||
Toast.show({
|
||||
type: "info",
|
||||
@@ -76,14 +79,14 @@ export default function Job_ButtonStatusSection({
|
||||
type: "success",
|
||||
text1: response.message,
|
||||
});
|
||||
router.back();
|
||||
router.replace(path("review"));
|
||||
} else {
|
||||
Toast.show({
|
||||
type: "info",
|
||||
text1: "Info",
|
||||
text2: response.message,
|
||||
});
|
||||
router.back();
|
||||
router.back()
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
@@ -113,14 +116,14 @@ export default function Job_ButtonStatusSection({
|
||||
type: "success",
|
||||
text1: response.message,
|
||||
});
|
||||
router.back();
|
||||
router.replace(path("draft"));
|
||||
} else {
|
||||
Toast.show({
|
||||
type: "info",
|
||||
text1: "Info",
|
||||
text2: response.message,
|
||||
});
|
||||
router.back();
|
||||
router. back();
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
|
||||
91
screens/Job/MainViewStatus.tsx
Normal file
91
screens/Job/MainViewStatus.tsx
Normal file
@@ -0,0 +1,91 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BaseBox,
|
||||
LoaderCustom,
|
||||
ScrollableCustom,
|
||||
TextCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
|
||||
import { apiJobGetByStatus } from "@/service/api-client/api-job";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
export default function Job_MainViewStatus() {
|
||||
const { user } = useAuth();
|
||||
const { status } = useLocalSearchParams<{ status?: string }>();
|
||||
console.log("STATUS", status);
|
||||
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>(
|
||||
status || "publish"
|
||||
);
|
||||
const [listData, setListData] = useState<any[]>([]);
|
||||
const [isLoadList, setIsLoadList] = useState(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [user?.id, activeCategory])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
setIsLoadList(true);
|
||||
const response = await apiJobGetByStatus({
|
||||
authorId: user?.id as string,
|
||||
status: activeCategory as string,
|
||||
});
|
||||
setListData(response.data);
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setIsLoadList(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 headerComponent={scrollComponent} hideFooter>
|
||||
{isLoadList ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<TextCustom align="center">
|
||||
Tidak ada data {activeCategory}
|
||||
</TextCustom>
|
||||
) : (
|
||||
listData.map((e, i) => (
|
||||
<BaseBox
|
||||
key={i}
|
||||
paddingTop={20}
|
||||
paddingBottom={20}
|
||||
href={`/job/${e?.id}/${activeCategory}/detail`}
|
||||
>
|
||||
<TextCustom align="center" bold truncate size="large">
|
||||
{e?.title}
|
||||
</TextCustom>
|
||||
</BaseBox>
|
||||
))
|
||||
)}
|
||||
</ViewWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
108
screens/Job/MainViewStatus2.tsx
Normal file
108
screens/Job/MainViewStatus2.tsx
Normal file
@@ -0,0 +1,108 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { BaseBox, ScrollableCustom, TextCustom } 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 { apiJobGetByStatus } from "@/service/api-client/api-job";
|
||||
import { useFocusEffect, useLocalSearchParams } from "expo-router";
|
||||
import { useCallback, useState } from "react";
|
||||
import { RefreshControl, View } from "react-native";
|
||||
|
||||
export default function Job_MainViewStatus2() {
|
||||
const { user } = useAuth();
|
||||
const { status } = useLocalSearchParams<{ status?: string }>();
|
||||
console.log("STATUS", status);
|
||||
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>(
|
||||
status || "publish",
|
||||
);
|
||||
|
||||
// Setup pagination
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page) => {
|
||||
if (!user?.id) return { data: [] };
|
||||
|
||||
return await apiJobGetByStatus({
|
||||
authorId: user?.id as string,
|
||||
status: activeCategory as string,
|
||||
page: String(page),
|
||||
});
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||
dependencies: [user?.id, activeCategory],
|
||||
onError: (error) => console.error("[ERROR] Fetch job by status:", error),
|
||||
});
|
||||
|
||||
// Generate komponen
|
||||
const { ListEmptyComponent, ListFooterComponent } =
|
||||
createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
emptyMessage: `Tidak ada data ${activeCategory}`,
|
||||
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||
skeletonHeight: 100,
|
||||
});
|
||||
|
||||
// Render item job
|
||||
const renderJobItem = ({ item }: { item: any }) => (
|
||||
<BaseBox
|
||||
key={item.id}
|
||||
paddingTop={20}
|
||||
paddingBottom={20}
|
||||
href={`/job/${item?.id}/${activeCategory}/detail`}
|
||||
>
|
||||
<TextCustom align="center" bold truncate size="large">
|
||||
{item?.title}
|
||||
</TextCustom>
|
||||
</BaseBox>
|
||||
);
|
||||
|
||||
const handlePress = (item: any) => {
|
||||
setActiveCategory(item.value);
|
||||
// Reset pagination saat kategori berubah
|
||||
pagination.reset();
|
||||
};
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
pagination.onRefresh();
|
||||
}, [activeCategory])
|
||||
);
|
||||
|
||||
const scrollComponent = (
|
||||
<ScrollableCustom
|
||||
data={dummyMasterStatus.map((e, i) => ({
|
||||
id: i,
|
||||
label: e.label,
|
||||
value: e.value,
|
||||
}))}
|
||||
onButtonPress={handlePress}
|
||||
activeId={activeCategory as any}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<NewWrapper
|
||||
headerComponent={<View style={{ paddingTop: 8 }}>{scrollComponent}</View>}
|
||||
listData={pagination.listData}
|
||||
renderItem={renderJobItem}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
tintColor={MainColor.yellow}
|
||||
colors={[MainColor.yellow]}
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
/>
|
||||
}
|
||||
onEndReached={pagination.loadMore}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
hideFooter
|
||||
/>
|
||||
);
|
||||
}
|
||||
57
screens/Job/ScreenArchive.tsx
Normal file
57
screens/Job/ScreenArchive.tsx
Normal file
@@ -0,0 +1,57 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { BaseBox, LoaderCustom, TextCustom, ViewWrapper } from "@/components";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { apiJobGetAll } from "@/service/api-client/api-job";
|
||||
import { useFocusEffect } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
export default function Job_ScreenArchive() {
|
||||
const { user } = useAuth();
|
||||
const [listData, setListData] = useState<any[]>([]);
|
||||
const [isLoadData, setIsLoadData] = useState(false);
|
||||
|
||||
useFocusEffect(
|
||||
useCallback(() => {
|
||||
onLoadData();
|
||||
}, [user?.id])
|
||||
);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
setIsLoadData(true);
|
||||
const response = await apiJobGetAll({
|
||||
category: "archive",
|
||||
authorId: user?.id,
|
||||
});
|
||||
setListData(response.data);
|
||||
} catch (error) {
|
||||
console.log("[ERROR]", error);
|
||||
} finally {
|
||||
setIsLoadData(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ViewWrapper hideFooter>
|
||||
{isLoadData ? (
|
||||
<LoaderCustom />
|
||||
) : _.isEmpty(listData) ? (
|
||||
<TextCustom align="center">Anda tidak memiliki arsip</TextCustom>
|
||||
) : (
|
||||
listData.map((item, index) => (
|
||||
<BaseBox
|
||||
key={index}
|
||||
paddingTop={20}
|
||||
paddingBottom={20}
|
||||
href={`/job/${item.id}/archive`}
|
||||
>
|
||||
<TextCustom align="center" bold truncate size="large">
|
||||
{item?.title || "-"}
|
||||
</TextCustom>
|
||||
</BaseBox>
|
||||
))
|
||||
)}
|
||||
</ViewWrapper>
|
||||
);
|
||||
}
|
||||
76
screens/Job/ScreenArchive2.tsx
Normal file
76
screens/Job/ScreenArchive2.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import { BaseBox, TextCustom, ViewWrapper } from "@/components";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import { createPaginationComponents } from "@/helpers/paginationHelpers";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { usePagination } from "@/hooks/use-pagination";
|
||||
import { apiJobGetAll } from "@/service/api-client/api-job";
|
||||
import { useFocusEffect } from "expo-router";
|
||||
import _ from "lodash";
|
||||
import { useState } from "react";
|
||||
import { RefreshControl } from "react-native";
|
||||
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
|
||||
|
||||
export default function Job_ScreenArchive2() {
|
||||
const { user } = useAuth();
|
||||
|
||||
// Setup pagination
|
||||
const pagination = usePagination({
|
||||
fetchFunction: async (page) => {
|
||||
if (!user?.id) return { data: [] };
|
||||
|
||||
return await apiJobGetAll({
|
||||
category: "archive",
|
||||
authorId: user?.id,
|
||||
page: String(page),
|
||||
});
|
||||
},
|
||||
pageSize: PAGINATION_DEFAULT_TAKE,
|
||||
dependencies: [user?.id],
|
||||
onError: (error) => console.error("[ERROR] Fetch job archive:", error),
|
||||
});
|
||||
|
||||
// Generate komponen
|
||||
const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({
|
||||
loading: pagination.loading,
|
||||
refreshing: pagination.refreshing,
|
||||
listData: pagination.listData,
|
||||
emptyMessage: "Anda tidak memiliki arsip",
|
||||
skeletonCount: PAGINATION_DEFAULT_TAKE,
|
||||
skeletonHeight: 80,
|
||||
});
|
||||
|
||||
// Render item job
|
||||
const renderJobItem = ({ item }: { item: any }) => (
|
||||
<BaseBox
|
||||
key={item.id}
|
||||
paddingTop={20}
|
||||
paddingBottom={20}
|
||||
href={`/job/${item.id}/archive`}
|
||||
>
|
||||
<TextCustom align="center" bold truncate size="large">
|
||||
{item?.title || "-"}
|
||||
</TextCustom>
|
||||
</BaseBox>
|
||||
);
|
||||
|
||||
return (
|
||||
<NewWrapper
|
||||
listData={pagination.listData}
|
||||
renderItem={renderJobItem}
|
||||
refreshControl={
|
||||
<RefreshControl
|
||||
tintColor={MainColor.yellow}
|
||||
colors={[MainColor.yellow]}
|
||||
refreshing={pagination.refreshing}
|
||||
onRefresh={pagination.onRefresh}
|
||||
/>
|
||||
}
|
||||
onEndReached={pagination.loadMore}
|
||||
ListEmptyComponent={ListEmptyComponent}
|
||||
ListFooterComponent={ListFooterComponent}
|
||||
hideFooter
|
||||
/>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user