Compare commits

...

6 Commits

Author SHA1 Message Date
c570a19d84 Fix Wrapper
UI – Investment (User)
- app/(application)/(user)/investment/create.tsx
- app/(application)/(user)/investment/[id]/edit.tsx

### No Issue"
2026-02-05 17:30:17 +08:00
7415c8c8ce Fix path Notification admin
UI – Notifications (User)
- app/(application)/(user)/notifications/index.tsx
- screens/Notification/ScreenNotification_V2.tsx

UI – Notifications (Admin)
- screens/Admin/Notification-Admin/ScreenNotificationAdmin2.tsx

API
- service/api-notifications.ts

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

### No Issue
2026-02-05 16:37:50 +08:00
72a3d42013 Fix Loaddata Voting
UI – Voting (User)
- app/(application)/(user)/voting/(tabs)/index.tsx
- app/(application)/(user)/voting/(tabs)/history.tsx
- app/(application)/(user)/voting/(tabs)/contribution.tsx
- app/(application)/(user)/voting/create.tsx
- app/(application)/(user)/voting/[id]/edit.tsx
- app/(application)/(user)/voting/[id]/[status]/detail.tsx
- app/(application)/(user)/voting/[id]/list-of-contributor.tsx

UI – Event
- app/(application)/(user)/event/[id]/edit.tsx
- screens/Event/ScreenStatus.tsx

UI – Voting Screens (New)
- screens/Voting/ScreenBeranda.tsx
- screens/Voting/ScreenContribution.tsx
- screens/Voting/ScreenHistory.tsx
- screens/Voting/ScreenListOfContributor.tsx
- screens/Voting/ScreenStatus.tsx
- screens/Voting/ButtonStatusSection.tsx

UI – Job
- screens/Job/ButtonStatusSection.tsx
- screens/Job/MainViewStatus2.tsx

API Client
- service/api-client/api-voting.ts

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

### No Issue
2026-02-05 15:06:14 +08:00
d0abd14047 Fix Loaddata Voting
Voting – User
- app/(application)/(user)/voting/(tabs)/status.tsx
- app/(application)/(user)/voting/create.tsx

Screens – Voting
- screens/Voting/ButtonStatusSection.tsx

API Client
- service/api-client/api-voting.ts

Global
- app/+not-found.tsx
- styles/global-styles.ts

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

Untracked (New Files)
- screens/Voting/ScreenStatus.tsx

### No issue
2026-02-04 17:44:57 +08:00
5b2be20469 Fix Loaddata pada event dan perbaikan tampilan pada NewWrapper
Event – User
- app/(application)/(user)/event/(tabs)/contribution.tsx
- app/(application)/(user)/event/(tabs)/index.tsx
- app/(application)/(user)/event/[id]/list-of-participants.tsx

Voting – User
- app/(application)/(user)/voting/(tabs)/history.tsx

Components
- components/Notification/NotificationInitializer.tsx
- components/_ShareComponent/NewWrapper.tsx

Screens – Event
- screens/Event/BoxPublishSection.tsx
- screens/Event/ButtonStatusSection.tsx
- screens/Event/ScreenHistory.tsx
- screens/Event/ScreenStatus.tsx

Screens – Forum
- screens/Forum/ViewBeranda3.tsx

API Client
- service/api-client/api-event.ts

Styles
- styles/global-styles.ts

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

Untracked (New Files)
- screens/Event/ScreenBeranda.tsx
- screens/Event/ScreenContribution.tsx
- screens/Event/ScreenListOfParticipants.tsx

#### No Issue
2026-02-04 16:56:48 +08:00
60177a1087 Fix Component Datetime IOS
Components
- components/DateInput/DataTimeAndroid.tsx
- components/DateInput/DateTimeIOS.tsx
- components/Notification/NotificationInitializer.tsx

Screens
- screens/Event/ScreenStatus.tsx

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

### No Issue
2026-02-04 12:00:00 +08:00
43 changed files with 1187 additions and 850 deletions

View File

@@ -1,115 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { import Event_ScreenContribution from "@/screens/Event/ScreenContribution";
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";
export default function EventContribution() { 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 ( return (
<ViewWrapper hideFooter> <>
{isLoadList ? ( <Event_ScreenContribution />
<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>
); );
} }

View File

@@ -1,63 +1,9 @@
import { LoaderCustom, TextCustom } from "@/components"; import Event_ScreenBeranda from "@/screens/Event/ScreenBeranda";
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";
export default function EventBeranda() { 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 ( return (
<ViewWrapper <>
hideFooter <Event_ScreenBeranda />
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>
); );
} }

View File

@@ -58,7 +58,7 @@ export default function EventEdit() {
try { try {
setIsLoadData(true); setIsLoadData(true);
const response = await apiEventGetOne({ id: id as string }); const response = await apiEventGetOne({ id: id as string });
console.log("[DATA BY ID]", JSON.stringify(response, null, 2));
if (response.success) { if (response.success) {
setData(response.data); setData(response.data);
setSelectedDate(new Date(response.data.tanggal)); setSelectedDate(new Date(response.data.tanggal));

View File

@@ -1,110 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { import Event_ScreenListOfParticipants from "@/screens/Event/ScreenListOfParticipants";
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";
export default function EventListOfParticipants() { 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 ( return (
<ViewWrapper> <>
{loadtData && !listData ? ( <Event_ScreenListOfParticipants />
<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>
); );
} }

View File

@@ -1,15 +1,16 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { import {
BoxButtonOnFooter,
ButtonCenteredOnly, ButtonCenteredOnly,
ButtonCustom, ButtonCustom,
InformationBox, InformationBox,
LandscapeFrameUploaded, LandscapeFrameUploaded,
LoaderCustom, LoaderCustom,
NewWrapper,
SelectCustom, SelectCustom,
Spacing, Spacing,
StackCustom, StackCustom,
TextInputCustom, TextInputCustom
ViewWrapper,
} from "@/components"; } from "@/components";
import API_STRORAGE from "@/constants/base-url-api-strorage"; import API_STRORAGE from "@/constants/base-url-api-strorage";
import DIRECTORY_ID from "@/constants/directory-id"; import DIRECTORY_ID from "@/constants/directory-id";
@@ -198,7 +199,15 @@ export default function InvestmentEdit() {
}; };
return ( return (
<ViewWrapper> <NewWrapper
footerComponent={
<BoxButtonOnFooter>
<ButtonCustom isLoading={isLoading} onPress={handleSubmitUpdate}>
Simpan
</ButtonCustom>
</BoxButtonOnFooter>
}
>
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
<InformationBox text="Gambar investasi bisa berupa ilustrasi, poster atau foto terkait investasi." /> <InformationBox text="Gambar investasi bisa berupa ilustrasi, poster atau foto terkait investasi." />
<LandscapeFrameUploaded <LandscapeFrameUploaded
@@ -253,7 +262,8 @@ export default function InvestmentEdit() {
/> />
<TextInputCustom <TextInputCustom
disabled iconLeft="Rp."
// disabled
required required
placeholder="0" placeholder="0"
label="Total Lembar" label="Total Lembar"
@@ -339,11 +349,7 @@ export default function InvestmentEdit() {
)} )}
<Spacing /> <Spacing />
<ButtonCustom isLoading={isLoading} onPress={handleSubmitUpdate}>
Simpan
</ButtonCustom>
</StackCustom> </StackCustom>
<Spacing height={50} /> </NewWrapper>
</ViewWrapper>
); );
} }

View File

@@ -1,18 +1,19 @@
import { import {
BaseBox, BaseBox,
BoxButtonOnFooter,
ButtonCenteredOnly, ButtonCenteredOnly,
ButtonCustom, ButtonCustom,
CenterCustom, CenterCustom,
InformationBox, InformationBox,
LandscapeFrameUploaded, LandscapeFrameUploaded,
LoaderCustom, NewWrapper,
SelectCustom, SelectCustom,
Spacing, Spacing,
StackCustom, StackCustom,
TextCustom, TextCustom,
TextInputCustom, TextInputCustom,
ViewWrapper,
} from "@/components"; } from "@/components";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import DIRECTORY_ID from "@/constants/directory-id"; import DIRECTORY_ID from "@/constants/directory-id";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
@@ -184,7 +185,19 @@ export default function InvestmentCreate() {
// const [coba, setCoba] = useState(""); // const [coba, setCoba] = useState("");
return ( return (
<ViewWrapper> <NewWrapper
footerComponent={
<BoxButtonOnFooter>
<ButtonCustom
disabled={isLoading}
isLoading={isLoading}
onPress={() => handleSubmit()}
>
Simpan
</ButtonCustom>
</BoxButtonOnFooter>
}
>
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
<InformationBox text="Gambar investasi bisa berupa ilustrasi, poster atau foto terkait investasi." /> <InformationBox text="Gambar investasi bisa berupa ilustrasi, poster atau foto terkait investasi." />
<LandscapeFrameUploaded image={image as string} /> <LandscapeFrameUploaded image={image as string} />
@@ -264,7 +277,9 @@ export default function InvestmentCreate() {
<StackCustom gap={0}> <StackCustom gap={0}>
<TextInputCustom <TextInputCustom
disabled iconLeft="Rp."
// disabled
editable={false}
required required
placeholder="0" placeholder="0"
label="Total Lembar" label="Total Lembar"
@@ -291,7 +306,7 @@ export default function InvestmentCreate() {
/> />
{loadingMaster ? ( {loadingMaster ? (
<LoaderCustom /> <CustomSkeleton height={50} />
) : ( ) : (
<SelectCustom <SelectCustom
required required
@@ -313,7 +328,7 @@ export default function InvestmentCreate() {
)} )}
{loadingMaster ? ( {loadingMaster ? (
<LoaderCustom /> <CustomSkeleton height={50} />
) : ( ) : (
<SelectCustom <SelectCustom
required required
@@ -335,7 +350,7 @@ export default function InvestmentCreate() {
)} )}
{loadingMaster ? ( {loadingMaster ? (
<LoaderCustom /> <CustomSkeleton height={50} />
) : ( ) : (
<SelectCustom <SelectCustom
required required
@@ -357,15 +372,8 @@ export default function InvestmentCreate() {
)} )}
<Spacing /> <Spacing />
<ButtonCustom
disabled={isLoading}
isLoading={isLoading}
onPress={() => handleSubmit()}
>
Simpan
</ButtonCustom>
</StackCustom> </StackCustom>
<Spacing height={50} /> {/* <Spacing height={50} /> */}
</ViewWrapper> </NewWrapper>
); );
} }

View File

@@ -1,11 +1,11 @@
import ScreenNotification from "@/screens/Notification/ScreenNotification_V2";
import ScreenNotification_V1 from "@/screens/Notification/ScreenNotification_V1"; import ScreenNotification_V1 from "@/screens/Notification/ScreenNotification_V1";
import ScreenNotification_V2 from "@/screens/Notification/ScreenNotification_V2";
export default function Notification() { export default function Notification() {
return ( return (
<> <>
<ScreenNotification /> <ScreenNotification_V2 />
{/* <ScreenNotification_V1 /> */} {/* <ScreenNotification_V1 /> */}
</> </>
); );
} }

View File

@@ -1,59 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { import Voting_ScreenContribution from "@/screens/Voting/ScreenContribution";
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";
export default function VotingContribution() { export default function VotingContribution() {
const { user } = useAuth(); return <Voting_ScreenContribution />;
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>
);
} }

View File

@@ -1,77 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { LoaderCustom, TextCustom, ViewWrapper } from "@/components"; import Voting_ScreenHistory from "@/screens/Voting/ScreenHistory";
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";
export default function VotingHistory() { 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 ( return (
<ViewWrapper <>
hideFooter <Voting_ScreenHistory />
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>
); );
} }

View File

@@ -1,71 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { import Voting_ScreenBeranda from "@/screens/Voting/ScreenBeranda";
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";
export default function VotingBeranda() { 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 ( return (
<ViewWrapper <>
hideFooter <Voting_ScreenBeranda />
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>
); );
} }

View File

@@ -1,106 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { import Voting_ScreenStatus from "@/screens/Voting/ScreenStatus";
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";
export default function VotingStatus() { 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 ( return (
<ViewWrapper headerComponent={scrollComponent} hideFooter> <>
{loadingGetData ? ( <Voting_ScreenStatus />
<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>
); );
} }

View File

@@ -51,8 +51,6 @@ export default function VotingDetailStatus() {
setLoadingGetData(true); setLoadingGetData(true);
const response = await apiVotingGetOne({ id: id as string }); const response = await apiVotingGetOne({ id: id as string });
console.log("[DATA BY ID]", JSON.stringify(response, null, 2));
if (response.success) { if (response.success) {
setData(response.data); setData(response.data);
} }

View File

@@ -5,13 +5,14 @@ import {
ButtonCustom, ButtonCustom,
CenterCustom, CenterCustom,
LoaderCustom, LoaderCustom,
NewWrapper,
Spacing, Spacing,
StackCustom, StackCustom,
TextAreaCustom, TextAreaCustom,
TextCustom, TextCustom,
TextInputCustom, TextInputCustom
ViewWrapper,
} from "@/components"; } from "@/components";
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom"; import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_XLARGE } from "@/constants/constans-value"; import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
@@ -34,7 +35,7 @@ interface IEditData {
Voting_DaftarNamaVote?: [ Voting_DaftarNamaVote?: [
{ {
value?: string; value?: string;
} },
]; ];
} }
@@ -47,7 +48,7 @@ export default function VotingEdit() {
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
onLoadData(); onLoadData();
}, [id]) }, [id]),
); );
const onLoadData = async () => { const onLoadData = async () => {
@@ -188,9 +189,9 @@ export default function VotingEdit() {
}; };
return ( return (
<ViewWrapper footerComponent={buttonSubmit()}> <NewWrapper footerComponent={buttonSubmit()}>
{loadingGetData ? ( {loadingGetData ? (
<LoaderCustom /> <ListSkeletonComponent />
) : ( ) : (
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
<TextInputCustom <TextInputCustom
@@ -210,7 +211,7 @@ export default function VotingEdit() {
onChangeText={(text) => setData({ ...data, deskripsi: text })} onChangeText={(text) => setData({ ...data, deskripsi: text })}
/> />
<Spacing />
<DateTimePickerCustom <DateTimePickerCustom
minimumDate={new Date(Date.now())} minimumDate={new Date(Date.now())}
@@ -255,7 +256,7 @@ export default function VotingEdit() {
} }
</TextCustom> </TextCustom>
)} )}
<Spacing />
</StackCustom> </StackCustom>
{data?.Voting_DaftarNamaVote?.map((item: any, index: number) => ( {data?.Voting_DaftarNamaVote?.map((item: any, index: number) => (
@@ -270,7 +271,7 @@ export default function VotingEdit() {
...(data as any), ...(data as any),
Voting_DaftarNamaVote: data?.Voting_DaftarNamaVote?.map( Voting_DaftarNamaVote: data?.Voting_DaftarNamaVote?.map(
(item: any, i: any) => (item: any, i: any) =>
i === index ? { ...item, value } : item i === index ? { ...item, value } : item,
), ),
}) })
} }
@@ -327,6 +328,6 @@ export default function VotingEdit() {
<Spacing /> <Spacing />
</StackCustom> </StackCustom>
)} )}
</ViewWrapper> </NewWrapper>
); );
} }

View File

@@ -1,69 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { import Voting_ScreenListOfContributor from "@/screens/Voting/ScreenListOfContributor";
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";
export default function Voting_ListOfContributor() { export default function VotingListOfContributor() {
const { id } = useLocalSearchParams(); return <Voting_ScreenListOfContributor />;
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>
);
} }

View File

@@ -3,11 +3,12 @@ import {
BoxButtonOnFooter, BoxButtonOnFooter,
ButtonCustom, ButtonCustom,
CenterCustom, CenterCustom,
NewWrapper,
Spacing, Spacing,
StackCustom, StackCustom,
TextAreaCustom, TextAreaCustom,
TextInputCustom, TextInputCustom,
ViewWrapper ViewWrapper,
} from "@/components"; } from "@/components";
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom"; import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
@@ -79,7 +80,7 @@ export default function VotingCreate() {
type: "success", type: "success",
text1: "Data berhasil disimpan", text1: "Data berhasil disimpan",
}); });
router.replace("/(application)/(user)/voting/(tabs)/status?status=review"); router.replace("/voting/(tabs)/status?status=review");
} else { } else {
Toast.show({ Toast.show({
type: "error", type: "error",
@@ -106,7 +107,7 @@ export default function VotingCreate() {
}; };
return ( return (
<ViewWrapper footerComponent={buttonSubmit()}> <NewWrapper footerComponent={buttonSubmit()}>
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
<TextInputCustom <TextInputCustom
label="Judul Voting" label="Judul Voting"
@@ -142,7 +143,6 @@ export default function VotingCreate() {
} }
/> />
{listVote.map((item, index) => ( {listVote.map((item, index) => (
<TextInputCustom <TextInputCustom
key={index} key={index}
@@ -153,8 +153,8 @@ export default function VotingCreate() {
onChangeText={(value: any) => onChangeText={(value: any) =>
setListVote( setListVote(
listVote.map((item, i) => listVote.map((item, i) =>
i === index ? { ...item, value } : item i === index ? { ...item, value } : item,
) ),
) )
} }
/> />
@@ -198,6 +198,6 @@ export default function VotingCreate() {
<Spacing /> <Spacing />
</StackCustom> </StackCustom>
</ViewWrapper> </NewWrapper>
); );
} }

View File

@@ -1,11 +1,21 @@
import { BackButton, StackCustom, TextCustom, ViewWrapper } from "@/components"; import { BackButton, StackCustom, TextCustom, ViewWrapper } from "@/components";
import { Stack } from "expo-router"; import { router, Stack } from "expo-router";
export default function NotFoundScreen() { 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 ( return (
<> <>
<Stack.Screen <Stack.Screen
options={{ headerShown: true, title: "", headerLeft: () => <BackButton /> }} options={{ headerShown: true, title: "", headerLeft: () => <BackButton onPress={() => handleBack()} /> }}
/> />
<ViewWrapper> <ViewWrapper>
<StackCustom <StackCustom

View File

@@ -1,4 +1,3 @@
// DateTimeInput.tsx // DateTimeInput.tsx
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { GStyles } from "@/styles/global-styles"; import { GStyles } from "@/styles/global-styles";
@@ -7,7 +6,14 @@ import DateTimePicker, {
DateTimePickerEvent, DateTimePickerEvent,
} from "@react-native-community/datetimepicker"; } from "@react-native-community/datetimepicker";
import React, { useCallback, useState } from "react"; 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 Grid from "../Grid/GridCustom";
import TextCustom from "../Text/TextCustom"; import TextCustom from "../Text/TextCustom";
@@ -53,7 +59,7 @@ const DateTimeInput_Android: React.FC<DateTimeInputProps> = ({
const [selectedDate, setSelectedDate] = useState<Date>(value as any); const [selectedDate, setSelectedDate] = useState<Date>(value as any);
const [selectedTime, setSelectedTime] = 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 // Fungsi untuk menggabungkan tanggal dan waktu
const combineDateAndTime = useCallback((date: Date, time: Date): Date => { const combineDateAndTime = useCallback((date: Date, time: Date): Date => {
@@ -62,7 +68,7 @@ const DateTimeInput_Android: React.FC<DateTimeInputProps> = ({
time.getHours(), time.getHours(),
time.getMinutes(), time.getMinutes(),
time.getSeconds(), time.getSeconds(),
time.getMilliseconds() time.getMilliseconds(),
); );
return combined; return combined;
}, []); }, []);
@@ -92,10 +98,12 @@ const DateTimeInput_Android: React.FC<DateTimeInputProps> = ({
}; };
const toggleDatePicker = () => { const toggleDatePicker = () => {
Keyboard.dismiss();
setShowDate(!showDate); setShowDate(!showDate);
}; };
const toggleTimePicker = () => { const toggleTimePicker = () => {
Keyboard.dismiss();
setShowTime(!showTime); setShowTime(!showTime);
}; };

View File

@@ -1,13 +1,22 @@
// DateTimeInput.tsx // DateTimeInput.tsx
import { MainColor } from "@/constants/color-palet"; import { AccentColor, MainColor } from "@/constants/color-palet";
import { GStyles } from "@/styles/global-styles"; import { GStyles } from "@/styles/global-styles";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import DateTimePicker, { import DateTimePicker, {
DateTimePickerEvent, DateTimePickerEvent,
} from "@react-native-community/datetimepicker"; } from "@react-native-community/datetimepicker";
import dayjs from "dayjs"; import dayjs from "dayjs";
import React, { useState } from "react"; import React, { useState, useRef } from "react";
import { Button, StyleProp, Text, View, ViewStyle } from "react-native"; import {
Button,
StyleProp,
Text,
View,
ViewStyle,
Keyboard,
TouchableOpacity,
Modal,
} from "react-native";
import ClickableCustom from "../Clickable/ClickableCustom"; import ClickableCustom from "../Clickable/ClickableCustom";
import TextCustom from "../Text/TextCustom"; import TextCustom from "../Text/TextCustom";
@@ -50,20 +59,35 @@ const DateTimeInput_IOS: React.FC<DateTimeInputProps> = ({
}) => { }) => {
const [show, setShow] = useState(false); const [show, setShow] = useState(false);
const [selectedDate, setSelectedDate] = useState<Date | undefined>( 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) => { const handleConfirm = (event: any, date?: Date) => {
if (event.type === "set" && date !== undefined) { if (event.type === "set" && date !== undefined) {
setSelectedDate(date); // Hanya perbarui state sementara, bukan state utama
onChange(date as any); setTempSelectedDate(date);
} }
}; };
const handlePress = () => { const handlePress = () => {
// Sembunyikan keyboard sebelum menampilkan date picker
Keyboard.dismiss();
// Set state sementara ke nilai saat ini
setTempSelectedDate(selectedDate);
setShow(!show); setShow(!show);
}; };
// Fungsi untuk menangani klik di luar area picker
const handleOutsidePress = () => {
if (show) {
setShow(false);
}
};
return ( return (
<> <>
<ClickableCustom <ClickableCustom
@@ -112,84 +136,125 @@ const DateTimeInput_IOS: React.FC<DateTimeInputProps> = ({
))} ))}
</ClickableCustom> </ClickableCustom>
{show && ( <Modal
<> animationType="fade"
<View 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={{ style={{
position: "absolute", position: "absolute",
zIndex: 15, top: 0,
backgroundColor: "white",
borderRadius: 8,
padding: 10,
// top: 0,
bottom: 0, bottom: 0,
left: 0, left: 0,
right: 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, borderWidth: 1,
}} }}
onStartShouldSetResponder={() => true} // Mencegah event bubbling ke TouchableOpacity induk
onResponderRelease={() => {}} // Handler kosong untuk mencegah event bubbling
> >
{/* <View style={{ alignItems: "flex-start" }}> {/* <View
<Ionicons style={{
name="close" flexDirection: "row",
size={20} justifyContent: "space-between",
color="black" alignItems: "center",
onPress={() => { marginBottom: 10,
setShow(false); }}
setSelectedDate(undefined); >
<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> */} </View> */}
<DateTimePicker <DateTimePicker
value={selectedDate || new Date()} value={tempSelectedDate || new Date()}
mode={"datetime"} mode={"datetime"}
display="spinner" display="inline"
onChange={handleConfirm} onChange={handleConfirm}
minimumDate={minimumDate} minimumDate={minimumDate}
maximumDate={maximumDate} maximumDate={maximumDate}
themeVariant="light" themeVariant="light"
/> />
<View style={{ flexDirection: "row", gap: 10 }}> <View style={{ flexDirection: "row", gap: 10, marginTop: 15 }}>
<ClickableCustom <TouchableOpacity
onPress={() => { onPress={() => {
setShow(false) setShow(false);
setSelectedDate(undefined) // Kembalikan ke nilai sebelumnya jika batal
setTempSelectedDate(selectedDate);
}} }}
style={{ style={{
flex: 1,
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
padding: 12, padding: 12,
borderRadius: 10, borderRadius: 10,
backgroundColor: MainColor.placeholder, backgroundColor: MainColor.placeholder,
marginTop: 10,
width: "48%",
}} }}
> >
<TextCustom color="black">Batal</TextCustom> <TextCustom color="black">Batal</TextCustom>
</ClickableCustom> </TouchableOpacity>
<ClickableCustom <TouchableOpacity
onPress={() => { onPress={() => {
setShow(false) // Simpan nilai yang dipilih ke state utama hanya saat OK ditekan
onChange(selectedDate as any) setSelectedDate(tempSelectedDate);
onChange(tempSelectedDate as any);
setShow(false);
}} }}
style={{ style={{
flex: 1,
alignItems: "center", alignItems: "center",
justifyContent: "center", justifyContent: "center",
padding: 12, padding: 12,
borderRadius: 10, borderRadius: 10,
backgroundColor: MainColor.darkblue, backgroundColor: MainColor.darkblue,
marginTop: 10,
width: "48%",
}} }}
> >
<TextCustom>OK</TextCustom> <TextCustom>OK</TextCustom>
</ClickableCustom> </TouchableOpacity>
</View> </View>
</View> </View>
</> </View>
)} </Modal>
</> </>
); );
}; };

View File

@@ -49,9 +49,8 @@ export default function NotificationInitializer() {
const fcmToken = await getToken(messagingInstance); const fcmToken = await getToken(messagingInstance);
if (!fcmToken) { if (!fcmToken) {
console.warn("Tidak bisa mendapatkan FCM token"); console.log("Tidak bisa mendapatkan FCM token");
// logout(); // logout();
return;
} }
console.log("✅ FCM Token:", fcmToken); console.log("✅ FCM Token:", fcmToken);

View File

@@ -101,12 +101,14 @@ const NewWrapper = (props: NewWrapperProps) => {
renderItem={listProps.renderItem} renderItem={listProps.renderItem}
keyExtractor={ keyExtractor={
listProps.keyExtractor || listProps.keyExtractor ||
((item) => { ((item, index) => {
if (item.id == null) { if (item.id == null) {
console.warn("Item tanpa 'id':", item); 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}`;
}) })
} }

View File

@@ -1,25 +1,31 @@
<!-- Start Penerapan Pagination --> <!-- ===================== Start Penerapan Pagination ===================== -->
File utama: screens/Event/ScreenHistory.tsx File utama: screens/Voting/ScreenListOfContributor.tsx
Fun fecth: apiEventGetAll Function fecth: apiVotingContribution
File fetch: service/api-client/api-event.ts File function fetch: service/api-client/api-voting.ts
File komponen wrapper: components/_ShareComponent/NewWrapper.tsx File komponen wrapper: components/_ShareComponent/NewWrapper.tsx
File refrensi: screens/Job/MainViewStatus2.tsx
Terapkan pagination pada file "File utama" 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 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 Komponen pagination yang digunaka berada pada file hooks/use-pagination.tsx dan helpers/paginationHelpers.tsx
Perbaiki fetch "Fun fecth" , pada file "File fetch" Perbaiki fetch "Function fecth" , pada file "File function fetch"
Jika tidak ada props page maka tambahkan props page dan default page: "1" Jika tidak ada props page maka tambahkan props page dan default page: "1"
Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan tipe fitur yang sama
Gunakan bahasa indonesia pada cli agar saya mudah membacanya. Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
<!-- End Penerapan Pagination --> <!-- 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 --> <!-- Start Penerapan NewWrapper -->
Terapkan NewWrapper pada file: screens/Forum/DetailForum.tsx 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 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

View File

@@ -12,7 +12,7 @@ import {
import { IconPlus } from "@/components/_Icon"; import { IconPlus } from "@/components/_Icon";
import { IconDot } from "@/components/_Icon/IconComponent"; import { IconDot } from "@/components/_Icon/IconComponent";
import { AccentColor, MainColor } from "@/constants/color-palet"; import { AccentColor, MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value"; import { ICON_SIZE_SMALL, PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers"; import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { useNotificationStore } from "@/hooks/use-notification-store"; import { useNotificationStore } from "@/hooks/use-notification-store";
@@ -26,8 +26,6 @@ import _ from "lodash";
import { useState } from "react"; import { useState } from "react";
import { RefreshControl, View } from "react-native"; import { RefreshControl, View } from "react-native";
const PAGE_SIZE = 10;
const selectedCategory = (value: string) => { const selectedCategory = (value: string) => {
const category = listOfcategoriesAppNotification.find( const category = listOfcategoriesAppNotification.find(
(c) => c.value === value, (c) => c.value === value,
@@ -35,6 +33,37 @@ const selectedCategory = (value: string) => {
return category?.label; 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 = ({ const BoxNotification = ({
data, data,
activeCategory, activeCategory,
@@ -50,17 +79,22 @@ const BoxNotification = ({
<BaseBox <BaseBox
backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue} backgroundColor={data.isRead ? AccentColor.darkblue : AccentColor.blue}
onPress={() => { onPress={() => {
console.log( const newPath = fixPath({
"Notification >", deepLink: data.deepLink,
selectedCategory(activeCategory as string), categoryApp: data.kategoriApp,
); });
router.push(data.deepLink);
markAsRead(data.id); selectedCategory(activeCategory as string);
setListData((prev: any) => router.navigate(newPath as any);
prev.map((item: any) =>
item.id === data.id ? { ...item, isRead: true } : item, if (!data.isRead) {
), markAsRead(data.id);
); setListData((prev: any) =>
prev.map((item: any) =>
item.id === data.id ? { ...item, isRead: true } : item,
),
);
}
}} }}
> >
<StackCustom> <StackCustom>
@@ -97,7 +131,7 @@ export default function Admin_ScreenNotification2() {
page: String(page), page: String(page),
}); });
}, },
pageSize: PAGE_SIZE, pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [user?.id, activeCategory], dependencies: [user?.id, activeCategory],
onError: (error) => onError: (error) =>
console.error("[ERROR] Fetch admin notifications:", error), console.error("[ERROR] Fetch admin notifications:", error),
@@ -110,7 +144,7 @@ export default function Admin_ScreenNotification2() {
refreshing: pagination.refreshing, refreshing: pagination.refreshing,
listData: pagination.listData, listData: pagination.listData,
emptyMessage: "Belum ada notifikasi", emptyMessage: "Belum ada notifikasi",
skeletonCount: 5, skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100, skeletonHeight: 100,
}); });

View File

@@ -1,6 +1,7 @@
import { import {
AvatarUsernameAndOtherComponent, AvatarUsernameAndOtherComponent,
BoxWithHeaderSection, BoxWithHeaderSection,
Spacing,
StackCustom, StackCustom,
TextCustom, TextCustom,
} from "@/components"; } from "@/components";

View File

@@ -31,7 +31,7 @@ export default function Event_ButtonStatusSection({
type: "success", type: "success",
text1: response.message, text1: response.message,
}); });
router.back(); router.replace(`/event/(tabs)/status?status=draft`);
} else { } else {
Toast.show({ Toast.show({
type: "info", type: "info",
@@ -65,7 +65,7 @@ export default function Event_ButtonStatusSection({
type: "success", type: "success",
text1: response.message, text1: response.message,
}); });
router.back(); router.replace(`/event/(tabs)/status?status=review`);
} else { } else {
Toast.show({ Toast.show({
type: "info", type: "info",
@@ -99,7 +99,7 @@ export default function Event_ButtonStatusSection({
type: "success", type: "success",
text1: response.message, text1: response.message,
}); });
router.back(); router.replace(`/event/(tabs)/status?status=draft`);
} else { } else {
Toast.show({ Toast.show({
type: "info", type: "info",

View 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")} />
}
/>
);
}

View 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
/>
);
}

View File

@@ -2,6 +2,7 @@
import { ButtonCustom, Spacing, TextCustom } from "@/components"; import { ButtonCustom, Spacing, TextCustom } from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper"; import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { AccentColor, MainColor } from "@/constants/color-palet"; import { AccentColor, MainColor } from "@/constants/color-palet";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers"; import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { usePagination } from "@/hooks/use-pagination"; import { usePagination } from "@/hooks/use-pagination";
@@ -12,8 +13,6 @@ import _ from "lodash";
import { useState } from "react"; import { useState } from "react";
import { RefreshControl, View } from "react-native"; import { RefreshControl, View } from "react-native";
const PAGE_SIZE = 5;
export default function Event_ScreenHistory() { export default function Event_ScreenHistory() {
const [activeCategory, setActiveCategory] = useState<string | null>("all"); const [activeCategory, setActiveCategory] = useState<string | null>("all");
const { user } = useAuth(); const { user } = useAuth();
@@ -27,25 +26,26 @@ export default function Event_ScreenHistory() {
page: String(page), page: String(page),
}); });
}, },
pageSize: PAGE_SIZE, pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [user?.id, activeCategory], dependencies: [user?.id, activeCategory],
onError: (error) => console.error("[ERROR] Fetch event history:", error), onError: (error) => console.error("[ERROR] Fetch event history:", error),
}); });
// Generate komponen // Generate komponen
const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({ const { ListEmptyComponent, ListFooterComponent } =
loading: pagination.loading, createPaginationComponents({
refreshing: pagination.refreshing, loading: pagination.loading,
listData: pagination.listData, refreshing: pagination.refreshing,
emptyMessage: "Belum ada riwayat", listData: pagination.listData,
skeletonCount: 5, emptyMessage: "Belum ada riwayat",
skeletonHeight: 100, skeletonCount: PAGINATION_DEFAULT_TAKE,
}); skeletonHeight: 100,
});
// Render item event // Render item event
const renderEventItem = ({ item }: { item: any }) => ( const renderEventItem = ({ item }: { item: any }) => (
<Event_BoxPublishSection <Event_BoxPublishSection
key={item.id} key={item && item?.id}
data={item} data={item}
rightComponentAvatar={ rightComponentAvatar={
<TextCustom> <TextCustom>

View 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}
/>
);
}

View File

@@ -7,26 +7,23 @@ import {
TextCustom, TextCustom,
} from "@/components"; } from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper"; 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 { createPaginationComponents } from "@/helpers/paginationHelpers";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { usePagination } from "@/hooks/use-pagination"; import { usePagination } from "@/hooks/use-pagination";
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status"; import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
import { apiEventGetByStatus } from "@/service/api-client/api-event"; import { apiEventGetByStatus } from "@/service/api-client/api-event";
import { useFocusEffect, useLocalSearchParams } from "expo-router"; import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash"; import { useCallback, useState } from "react";
import { useState } from "react";
import { RefreshControl, View } from "react-native"; import { RefreshControl, View } from "react-native";
const PAGE_SIZE = 10;
export default function Event_ScreenStatus() { export default function Event_ScreenStatus() {
const { user } = useAuth(); const { user } = useAuth();
const { status } = useLocalSearchParams<{ status?: string }>(); const { status } = useLocalSearchParams<{ status?: string }>();
const id = user?.id || ""; const id = user?.id || "";
const [activeCategory, setActiveCategory] = useState<string | null>( const [activeCategory, setActiveCategory] = useState<string | null>(
status || "publish" status || "publish",
); );
// Setup pagination // Setup pagination
@@ -40,20 +37,21 @@ export default function Event_ScreenStatus() {
page: String(page), page: String(page),
}); });
}, },
pageSize: PAGE_SIZE, pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [id, activeCategory], dependencies: [id, activeCategory],
onError: (error) => console.error("[ERROR] Fetch event by status:", error), onError: (error) => console.error("[ERROR] Fetch event by status:", error),
}); });
// Generate komponen // Generate komponen
const { ListEmptyComponent, ListFooterComponent } = createPaginationComponents({ const { ListEmptyComponent, ListFooterComponent } =
loading: pagination.loading, createPaginationComponents({
refreshing: pagination.refreshing, loading: pagination.loading,
listData: pagination.listData, refreshing: pagination.refreshing,
emptyMessage: `Tidak ada data ${activeCategory}`, listData: pagination.listData,
skeletonCount: 5, emptyMessage: `Tidak ada data ${activeCategory}`,
skeletonHeight: 100, skeletonCount: PAGINATION_DEFAULT_TAKE,
}); skeletonHeight: 100,
});
// Render item event // Render item event
const renderEventItem = ({ item }: { item: any }) => ( const renderEventItem = ({ item }: { item: any }) => (
@@ -86,6 +84,12 @@ export default function Event_ScreenStatus() {
pagination.reset(); pagination.reset();
}; };
useFocusEffect(
useCallback(() => {
pagination.onRefresh();
}, [activeCategory])
);
const tabsComponent = ( const tabsComponent = (
<ScrollableCustom <ScrollableCustom
data={dummyMasterStatus.map((e, i) => ({ data={dummyMasterStatus.map((e, i) => ({
@@ -100,11 +104,8 @@ export default function Event_ScreenStatus() {
return ( return (
<NewWrapper <NewWrapper
headerComponent={ hideFooter
<View style={{ paddingTop: 8 }}> headerComponent={<View style={{ paddingTop: 8 }}>{tabsComponent}</View>}
{tabsComponent}
</View>
}
listData={pagination.listData} listData={pagination.listData}
renderItem={renderEventItem} renderItem={renderEventItem}
refreshControl={ refreshControl={

View File

@@ -2,7 +2,7 @@ import {
AvatarComp, AvatarComp,
BackButton, BackButton,
FloatingButton, FloatingButton,
SearchInput SearchInput,
} from "@/components"; } from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper"; import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
@@ -101,8 +101,9 @@ export default function Forum_ViewBeranda3() {
/> />
<NewWrapper <NewWrapper
hideFooter
headerComponent={ headerComponent={
<View style={{ paddingTop: 8 }}> <View style={{ paddingTop: 8 }}>
<SearchInput <SearchInput
placeholder="Cari topik diskusi" placeholder="Cari topik diskusi"
onChangeText={_.debounce((text) => setSearch(text), 500)} onChangeText={_.debounce((text) => setSearch(text), 500)}

View File

@@ -20,6 +20,9 @@ export default function Job_ButtonStatusSection({
onSetLoading: (value: boolean) => void; onSetLoading: (value: boolean) => void;
isArchive?: boolean; isArchive?: boolean;
}) { }) {
const path : any =(status: string) => {
return `/job/(tabs)/status?status=${status}`
}
const handleBatalkanReview = () => { const handleBatalkanReview = () => {
AlertDefaultSystem({ AlertDefaultSystem({
title: "Batalkan Review", title: "Batalkan Review",
@@ -39,7 +42,7 @@ export default function Job_ButtonStatusSection({
type: "success", type: "success",
text1: response.message, text1: response.message,
}); });
router.back(); router.replace(path("draft"));
} else { } else {
Toast.show({ Toast.show({
type: "info", type: "info",
@@ -76,14 +79,14 @@ export default function Job_ButtonStatusSection({
type: "success", type: "success",
text1: response.message, text1: response.message,
}); });
router.back(); router.replace(path("review"));
} else { } else {
Toast.show({ Toast.show({
type: "info", type: "info",
text1: "Info", text1: "Info",
text2: response.message, text2: response.message,
}); });
router.back(); router.back()
} }
} catch (error) { } catch (error) {
console.log("[ERROR]", error); console.log("[ERROR]", error);
@@ -113,14 +116,14 @@ export default function Job_ButtonStatusSection({
type: "success", type: "success",
text1: response.message, text1: response.message,
}); });
router.back(); router.replace(path("draft"));
} else { } else {
Toast.show({ Toast.show({
type: "info", type: "info",
text1: "Info", text1: "Info",
text2: response.message, text2: response.message,
}); });
router.back(); router. back();
} }
} catch (error) { } catch (error) {
console.log("[ERROR]", error); console.log("[ERROR]", error);

View File

@@ -62,19 +62,17 @@ export default function Job_MainViewStatus2() {
</BaseBox> </BaseBox>
); );
// useFocusEffect(
// useCallback(() => {
// // Reset and load first page when category changes
// pagination.reset();
// // pagination.onRefresh();
// }, [activeCategory]),
// );
const handlePress = (item: any) => { const handlePress = (item: any) => {
setActiveCategory(item.value); setActiveCategory(item.value);
// Reset pagination saat kategori berubah // Reset pagination saat kategori berubah
pagination.reset(); pagination.reset();
}; };
useFocusEffect(
useCallback(() => {
pagination.onRefresh();
}, [activeCategory])
);
const scrollComponent = ( const scrollComponent = (
<ScrollableCustom <ScrollableCustom

View File

@@ -52,6 +52,9 @@ const fixPath = ({
return deepLink; return deepLink;
} }
console.log("Category App", categoryApp);
console.log("Deep Link", deepLink);
const separator = deepLink.includes("?") ? "&" : "?"; const separator = deepLink.includes("?") ? "&" : "?";
const fixedPath = `${deepLink}${separator}from=notifications&category=${_.lowerCase( const fixedPath = `${deepLink}${separator}from=notifications&category=${_.lowerCase(
@@ -83,8 +86,8 @@ const BoxNotification = ({
categoryApp: data.kategoriApp, categoryApp: data.kategoriApp,
}); });
router.navigate(newPath as any);
selectedCategory(activeCategory as string); selectedCategory(activeCategory as string);
router.navigate(newPath as any);
if (!data.isRead) { if (!data.isRead) {
markAsRead(data.id); markAsRead(data.id);
@@ -112,7 +115,7 @@ const BoxNotification = ({
); );
}; };
export default function ScreenNotification() { export default function ScreenNotification_V2() {
const { user } = useAuth(); const { user } = useAuth();
const { category } = useLocalSearchParams<{ category?: string }>(); const { category } = useLocalSearchParams<{ category?: string }>();
const [activeCategory, setActiveCategory] = useState<string | null>( const [activeCategory, setActiveCategory] = useState<string | null>(
@@ -140,7 +143,7 @@ export default function ScreenNotification() {
// useFocusEffect( // useFocusEffect(
// useCallback(() => { // useCallback(() => {
// // Reset and load first page when category changes // // Reset and load first page when category changes
// pagination.reset();
// pagination.onRefresh(); // pagination.onRefresh();
// }, [activeCategory]), // }, [activeCategory]),
// ); // );
@@ -171,7 +174,7 @@ export default function ScreenNotification() {
listData: pagination.listData, listData: pagination.listData,
isInitialLoad: pagination.isInitialLoad, isInitialLoad: pagination.isInitialLoad,
emptyMessage: "Belum ada notifikasi", emptyMessage: "Belum ada notifikasi",
skeletonCount: 5, skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100, skeletonHeight: 100,
}); });
@@ -237,7 +240,6 @@ export default function ScreenNotification() {
}, },
]} ]}
onPressItem={(item: any) => { onPressItem={(item: any) => {
// console.log("Item", item.value);
if (item.value === "read-all") { if (item.value === "read-all") {
AlertDefaultSystem({ AlertDefaultSystem({
title: "Tandai Semua Dibaca", title: "Tandai Semua Dibaca",

View File

@@ -3,7 +3,7 @@ import {
apiVotingDelete, apiVotingDelete,
apiVotingUpdateStatus, apiVotingUpdateStatus,
} from "@/service/api-client/api-voting"; } from "@/service/api-client/api-voting";
import { router } from "expo-router"; import { RelativePathString, router } from "expo-router";
import Toast from "react-native-toast-message"; import Toast from "react-native-toast-message";
export default function Voting_ButtonStatusSection({ export default function Voting_ButtonStatusSection({
@@ -17,6 +17,10 @@ export default function Voting_ButtonStatusSection({
isLoading: boolean; isLoading: boolean;
onSetLoading: (value: boolean) => void; onSetLoading: (value: boolean) => void;
}) { }) {
const path: any = (status: string) => {
return `/voting/(tabs)/status?status=${status}`;
};
const handleBatalkanReview = () => { const handleBatalkanReview = () => {
AlertDefaultSystem({ AlertDefaultSystem({
title: "Batalkan Review", title: "Batalkan Review",
@@ -34,9 +38,9 @@ export default function Voting_ButtonStatusSection({
if (response?.success) { if (response?.success) {
Toast.show({ Toast.show({
type: "success", type: "success",
text1: response.message, text1: "Berhasil batalkan review",
}); });
router.back(); router.replace(path("draft"));
} else { } else {
Toast.show({ Toast.show({
type: "info", type: "info",
@@ -71,9 +75,9 @@ export default function Voting_ButtonStatusSection({
if (response?.success) { if (response?.success) {
Toast.show({ Toast.show({
type: "success", type: "success",
text1: response.message, text1: "Berhasil ajukan review",
}); });
router.back(); router.replace(path("review"));
} else { } else {
Toast.show({ Toast.show({
type: "info", type: "info",
@@ -108,9 +112,9 @@ export default function Voting_ButtonStatusSection({
if (response?.success) { if (response?.success) {
Toast.show({ Toast.show({
type: "success", type: "success",
text1: response.message, text1: "Berhasil edit kembali",
}); });
router.back(); router.replace(path("draft"));
} else { } else {
Toast.show({ Toast.show({
type: "info", type: "info",
@@ -135,31 +139,31 @@ export default function Voting_ButtonStatusSection({
textLeft: "Batal", textLeft: "Batal",
textRight: "Hapus", textRight: "Hapus",
onPressRight: async () => { onPressRight: async () => {
try { try {
onSetLoading(true); onSetLoading(true);
const response = await apiVotingDelete({ const response = await apiVotingDelete({
id: id as string, id: id as string,
}); });
if (response?.success) { if (response?.success) {
Toast.show({ Toast.show({
type: "success", type: "success",
text1: response.message, text1: "Berhasil hapus data",
}); });
router.back(); router.back();
} else { } else {
Toast.show({ Toast.show({
type: "info", type: "info",
text1: "Info", text1: "Info",
text2: response.message, text2: response.message,
}); });
router.back(); router.back();
} }
} catch (error) { } catch (error) {
console.log("[ERROR]", error); console.log("[ERROR]", error);
} finally { } finally {
onSetLoading(false); onSetLoading(false);
} }
}, },
}); });
}; };

View File

@@ -0,0 +1,82 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { FloatingButton, SearchInput } from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { useAuth } from "@/hooks/use-auth";
import { usePagination } from "@/hooks/use-pagination";
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
import { apiVotingGetAll } from "@/service/api-client/api-voting";
import { router } from "expo-router";
import { useMemo, useState } from "react";
import { RefreshControl } from "react-native";
export default function Voting_ScreenBeranda() {
const { user } = useAuth();
const [search, setSearch] = useState("");
const pagination = usePagination({
fetchFunction: async (page, searchQuery) => {
return await apiVotingGetAll({
search: searchQuery || "",
category: "beranda",
userLoginId: user?.id,
page: String(page),
});
},
pageSize: 5,
searchQuery: search,
dependencies: [user?.id],
onError: (error) => console.error("[ERROR] Fetch voting:", error),
});
// Gunakan helper untuk membuat komponen-komponen pagination
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,
isInitialLoad: pagination.isInitialLoad,
});
// Render item untuk FlatList
const renderItem = useMemo(
() =>
({ item }: { item: any }) =>
(
<Voting_BoxPublishSection
data={item}
key={item.id}
href={`/voting/${item.id}`}
/>
),
[],
);
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onEndReached={pagination.loadMore}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
hideFooter
floatingButton={
<FloatingButton onPress={() => router.push("/voting/create")} />
}
headerComponent={
<SearchInput placeholder="Cari voting" onChangeText={setSearch} />
}
/>
);
}

View File

@@ -0,0 +1,70 @@
/* eslint-disable react-hooks/exhaustive-deps */
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { useAuth } from "@/hooks/use-auth";
import { usePagination } from "@/hooks/use-pagination";
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
import { apiVotingGetAll } from "@/service/api-client/api-voting";
import { useMemo } from "react";
import { RefreshControl } from "react-native";
export default function Voting_ScreenContribution() {
const { user } = useAuth();
const pagination = usePagination({
fetchFunction: async (page) => {
return await apiVotingGetAll({
category: "contribution",
authorId: user?.id as string,
page: String(page),
});
},
pageSize: 4,
dependencies: [user?.id],
onError: (error) => console.error("[ERROR] Fetch contribution:", error),
});
// Gunakan helper untuk membuat komponen-komponen pagination
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
emptyMessage: "Tidak ada kontribusi",
emptySearchMessage: "Tidak ada hasil pencarian",
skeletonCount: 5,
skeletonHeight: 200,
isInitialLoad: pagination.isInitialLoad,
});
// Render item untuk FlatList
const renderItem = useMemo(
() =>
({ item }: { item: any }) =>
(
<Voting_BoxPublishSection
data={item}
key={item.id}
href={`/voting/${item.id}/contribution`}
/>
),
[],
);
return (
<NewWrapper
listData={pagination.listData}
renderItem={renderItem}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
onEndReached={pagination.loadMore}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
hideFooter
/>
);
}

View File

@@ -0,0 +1,114 @@
/* 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 Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
import { apiVotingGetAll } from "@/service/api-client/api-voting";
import { useState } from "react";
import { RefreshControl, View } from "react-native";
export default function Voting_ScreenHistory() {
const [activeCategory, setActiveCategory] = useState<string | null>("all");
const { user } = useAuth();
// Setup pagination
const pagination = usePagination({
fetchFunction: async (page) => {
return await apiVotingGetAll({
category: activeCategory === "all" ? "all-history" : "my-history",
authorId: user?.id as string,
page: String(page),
});
},
pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [user?.id, activeCategory],
onError: (error) => console.error("[ERROR] Fetch voting 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 voting
const renderVotingItem = ({ item }: { item: any }) => (
<Voting_BoxPublishSection
key={item && item?.id}
data={item}
href={`/voting/${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={renderVotingItem}
refreshControl={
<RefreshControl
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
hideFooter
/>
);
}

View File

@@ -0,0 +1,80 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AvatarUsernameAndOtherComponent,
BadgeCustom,
BaseBox,
Spacing,
TextCustom,
} from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { PAGINATION_DEFAULT_TAKE } from "@/constants/constans-value";
import { createPaginationComponents } from "@/helpers/paginationHelpers";
import { usePagination } from "@/hooks/use-pagination";
import { apiVotingContribution } from "@/service/api-client/api-voting";
import { useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { RefreshControl, View } from "react-native";
export default function Voting_ScreenListOfContributor() {
const { id } = useLocalSearchParams();
// Setup pagination
const pagination = usePagination({
fetchFunction: async (page) => {
return await apiVotingContribution({
id: id as string,
authorId: "",
category: "list",
page: String(page),
});
},
pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [id],
onError: (error) =>
console.error("[ERROR] Fetch voting contributors:", error),
});
// Generate komponen
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
emptyMessage: "Tidak ada kontributor",
skeletonCount: PAGINATION_DEFAULT_TAKE,
skeletonHeight: 100,
});
// Render item contributor
const renderContributorItem = ({ item }: { item: any }) => (
<BaseBox paddingTop={5} paddingBottom={5} key={item?.id}>
<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>
);
return (
<NewWrapper
hideFooter
listData={pagination.listData}
renderItem={renderContributorItem}
refreshControl={
<RefreshControl
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
/>
);
}

View File

@@ -0,0 +1,132 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BadgeCustom,
BaseBox,
ScrollableCustom,
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 { 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 { useCallback, useState } from "react";
import { RefreshControl, View } from "react-native";
const PAGE_SIZE = 6
export default function Voting_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 apiVotingGetByStatus({
id: id as string,
status: activeCategory!,
page: String(page),
});
},
pageSize: PAGINATION_DEFAULT_TAKE,
dependencies: [id, activeCategory],
onError: (error) => console.error("[ERROR] Fetch voting by status:", error),
});
// Generate komponen
const { ListEmptyComponent, ListFooterComponent } =
createPaginationComponents({
loading: pagination.loading,
refreshing: pagination.refreshing,
listData: pagination.listData,
emptyMessage: `Tidak ada data ${activeCategory}`,
skeletonCount: 5,
skeletonHeight: 100,
});
// Render item voting
const renderVotingItem = ({ item }: { item: any }) => (
<BaseBox
key={item.id}
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>
);
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={renderVotingItem}
refreshControl={
<RefreshControl
tintColor={MainColor.yellow}
colors={[MainColor.yellow]}
refreshing={pagination.refreshing}
onRefresh={pagination.onRefresh}
/>
}
onEndReached={pagination.loadMore}
ListEmptyComponent={ListEmptyComponent}
ListFooterComponent={ListFooterComponent}
hideFooter
/>
);
}

View File

@@ -117,9 +117,16 @@ export async function apiEventJoin({
} }
} }
export async function apiEventListOfParticipants({ id }: { id?: string }) { export async function apiEventListOfParticipants({
id,
page = "1"
}: {
id?: string;
page?: string;
}) {
try { try {
const response = await apiConfig.get(`/mobile/event/${id}/participants`); const pageParam = page ? `?page=${page}` : "";
const response = await apiConfig.get(`/mobile/event/${id}/participants${pageParam}`);
return response.data; return response.data;
} catch (error) { } catch (error) {
throw error; throw error;

View File

@@ -14,12 +14,14 @@ export async function apiVotingCreate(data: any) {
export async function apiVotingGetByStatus({ export async function apiVotingGetByStatus({
id, id,
status, status,
page = "1",
}: { }: {
id: string; id: string;
status: string; status: string;
page?: string;
}) { }) {
try { try {
const response = await apiConfig.get(`/mobile/voting/${id}/${status}`); const response = await apiConfig.get(`/mobile/voting/${id}/${status}?page=${page}`);
return response.data; return response.data;
} catch (error) { } catch (error) {
throw error; throw error;
@@ -79,14 +81,15 @@ export async function apiVotingUpdateData({
} }
} }
export async function apiVotingGetAll({ search, category, authorId, userLoginId }: { search?: string, category: "beranda" | "contribution" | "all-history" | "my-history", authorId?: string, userLoginId?: string }) { export async function apiVotingGetAll({ search, category, authorId, userLoginId, page = "1" }: { search?: string, category: "beranda" | "contribution" | "all-history" | "my-history", authorId?: string, userLoginId?: string, page?: string }) {
try { try {
console.log("userLoginId", userLoginId); console.log("userLoginId", userLoginId);
const categoryQuery = category ? `?category=${category}` : ""; const categoryQuery = category ? `?category=${category}` : "";
const searchQuery = search ? `&search=${search}` : ""; const searchQuery = search ? `&search=${search}` : "";
const authorIdQuery = authorId ? `&authorId=${authorId}` : ""; const authorIdQuery = authorId ? `&authorId=${authorId}` : "";
const userLoginIdQuery = userLoginId ? `&userLoginId=${userLoginId}` : ""; const userLoginIdQuery = userLoginId ? `&userLoginId=${userLoginId}` : "";
const response = await apiConfig.get(`/mobile/voting${categoryQuery}${searchQuery}${authorIdQuery}${userLoginIdQuery}`); const pageQuery = `&page=${page}`;
const response = await apiConfig.get(`/mobile/voting${categoryQuery}${searchQuery}${authorIdQuery}${userLoginIdQuery}${pageQuery}`);
return response.data; return response.data;
} catch (error) { } catch (error) {
throw error; throw error;
@@ -108,15 +111,17 @@ export async function apiVotingContribution({
id, id,
authorId, authorId,
category, category,
page = "1",
}: { }: {
id: string; id: string;
authorId: string; authorId: string;
category: "list" | "checked"; category: "list" | "checked";
page?: string;
}) { }) {
const query = const query =
category === "list" category === "list"
? "?category=list" ? `?category=list&page=${page}`
: `?category=checked&authorId=${authorId}`; : `?category=checked&authorId=${authorId}&page=${page}`;
try { try {
const response = await apiConfig.get( const response = await apiConfig.get(
`/mobile/voting/${id}/contribution${query}` `/mobile/voting/${id}/contribution${query}`

View File

@@ -47,9 +47,8 @@ export async function apiGetNotificationsById({
category: TypeNotificationCategoryApp; category: TypeNotificationCategoryApp;
page?: string; page?: string;
}) { }) {
console.log("ID", id);
console.log("Category", category); console.log("Category", category);
console.log("Page", page);
try { try {
const response = await apiConfig.get( const response = await apiConfig.get(

View File

@@ -17,7 +17,9 @@ export const GStyles = StyleSheet.create({
container: { container: {
flex: 1, flex: 1,
paddingInline: PADDING_MEDIUM, paddingInline: PADDING_MEDIUM,
paddingBlock: PADDING_EXTRA_SMALL, paddingTop: PADDING_EXTRA_SMALL,
paddingBottom: 5,
// paddingBlock: PADDING_EXTRA_SMALL,
backgroundColor: MainColor.darkblue, backgroundColor: MainColor.darkblue,
}, },
containerWithBackground: { containerWithBackground: {