Compare commits

...

8 Commits

Author SHA1 Message Date
c2682246d6 Fix notifikasi forum
FixL
 modified:   app/(application)/(user)/_layout.tsx
        modified:   app/(application)/(user)/forum/[id]/index.tsx
        modified:   app/(application)/(user)/forum/create.tsx
        modified:   app/(application)/admin/forum/[id]/list-report-posting.tsx
        modified:   screens/Home/tabsList.ts
        modified:   service/api-admin/api-admin-forum.ts
        modified:   service/api-client/api-forum.ts

Add:
 app/(application)/(user)/forum/[id]/preview-report-posting.tsx
        types/type-forum.ts

### No Issue
2026-01-17 15:59:00 +08:00
465e01015e Penambahan notifikasi untuk fitur voting
Fix:
- app/(application)/(user)/event/[id]/publish.tsx
- app/(application)/(user)/voting/(tabs)/_layout.tsx
- app/(application)/(user)/voting/(tabs)/status.tsx
- app/(application)/(user)/voting/[id]/index.tsx
- app/(application)/(user)/voting/create.tsx
- app/(application)/admin/voting/[id]/[status]/index.tsx
- app/(application)/admin/voting/[id]/[status]/reject-input.tsx
- screens/Admin/Event/funUpdateStatus.ts
- screens/Admin/Voting/funUpdateStatus.ts
- service/api-admin/api-admin-voting.ts
- types/type-collect-other.ts

### No Issue
2026-01-15 17:39:39 +08:00
3b15871ad4 Notifikasi join event
Fix:
modified:   app/(application)/(user)/event/[id]/history.tsx
        modified:   app/(application)/(user)/event/[id]/publish.tsx
        modified:   app/(application)/(user)/event/create.tsx
        modified:   app/(application)/(user)/notifications/index.tsx

### No Issue
2026-01-15 13:54:14 +08:00
9123e73606 Fix bug pada beberapa halaman
Fix:
modified:   app.config.js
        modified:   app/(application)/(user)/donation/[id]/[status]/detail.tsx
        modified:   app/(application)/(user)/investment/(tabs)/my-holding.tsx
        modified:   app/(application)/(user)/investment/[id]/[status]/detail.tsx
        modified:   app/(application)/(user)/investment/[id]/index.tsx
        modified:   app/(application)/admin/event/[id]/[status]/index.tsx
        modified:   components/DateInput/DateTimePickerCustom.tsx
        modified:   ios/HIPMIBadungConnect/Info.plist
        modified:   screens/Invesment/BoxProgressSection.tsx

### No Issue
2026-01-14 17:41:20 +08:00
6e2046467f Penerapan notifikasi di event
Add:
components/Button/BackButtonFromNotification.tsx
types/type-collect-other.ts

Fix:
- android/app/build.gradle
- app/(application)/(user)/_layout.tsx
- app/(application)/(user)/event/(tabs)/_layout.tsx
- app/(application)/(user)/event/(tabs)/status.tsx
- app/(application)/(user)/event/create.tsx
- app/(application)/(user)/job/(tabs)/_layout.tsx
- app/(application)/(user)/notifications/index.tsx
- app/(application)/admin/event/[id]/[status]/index.tsx
- app/(application)/admin/event/[id]/reject-input.tsx
- app/(application)/admin/notification/index.tsx
- components/Notification/NotificationInitializer.tsx
- hipmi-note.md
- hooks/use-notification-store.tsx
- screens/Admin/Event/funUpdateStatus.ts
- service/api-notifications.ts
- utils/formatChatTime.ts

### No Issue
2026-01-13 17:41:30 +08:00
ca33dd83bb Perbaikan untuk apple store dan push notifikasi ke preview
Fix:
- app.config.js
- app/(application)/(user)/_layout.tsx
- app/(application)/admin/notification/index.tsx
- eas.json
- ios/HIPMIBadungConnect.xcodeproj/project.pbxproj
- ios/HIPMIBadungConnect/Info.plist

### No Issue
2026-01-12 17:40:15 +08:00
ea3fbdc541 Penambahan metode login dengan menerapkan EULA di awal
Add:
app/eula.tsx
screens/Authentication/EULAView.tsx

Fix:
- context/AuthContext.tsx
- screens/Authentication/RegisterView.tsx
- screens/RootLayout/AppRoot.tsx
- service/api-config.ts

### No Issue
2026-01-09 17:44:13 +08:00
33cd47aaed Fix mobile notification:
> - Bug penerima pesan 2 kali

Fix:
modified:   hooks/use-foreground-notifications.ts

### No Issue
2026-01-09 14:46:21 +08:00
54 changed files with 1051 additions and 282 deletions

View File

@@ -100,7 +100,7 @@ packagingOptions {
applicationId 'com.bip.hipmimobileapp' applicationId 'com.bip.hipmimobileapp'
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 3 versionCode 4
versionName "1.0.1" versionName "1.0.1"
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\"" buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""

View File

@@ -21,7 +21,7 @@ export default {
"Aplikasi membutuhkan akses lokasi untuk menampilkan peta.", "Aplikasi membutuhkan akses lokasi untuk menampilkan peta.",
}, },
associatedDomains: ["applinks:cld-dkr-staging-hipmi.wibudev.com"], associatedDomains: ["applinks:cld-dkr-staging-hipmi.wibudev.com"],
buildNumber: "17", buildNumber: "19",
}, },
android: { android: {
@@ -32,7 +32,7 @@ export default {
}, },
edgeToEdgeEnabled: true, edgeToEdgeEnabled: true,
package: "com.bip.hipmimobileapp", package: "com.bip.hipmimobileapp",
versionCode: 3, versionCode: 4,
// softwareKeyboardLayoutMode: 'resize', // option: untuk mengatur keyboard pada room chst collaboration // softwareKeyboardLayoutMode: 'resize', // option: untuk mengatur keyboard pada room chst collaboration
intentFilters: [ intentFilters: [
{ {

View File

@@ -53,30 +53,35 @@ export default function UserLayout() {
/> />
{/* ========== Notification Section ========= */} {/* ========== Notification Section ========= */}
<Stack.Screen
{/* DIPINDAH DI FILE NOTIFICATION USER */}
{/* <Stack.Screen
name="notifications/index" name="notifications/index"
options={{ options={{
title: "Notifikasi", title: "Notifikasi",
headerLeft: () => <BackButton />, headerLeft: () => <BackButton />,
headerRight: () => ( // headerRight: () => (
<IconPlus // <IconPlus
color={MainColor.yellow} // color={MainColor.yellow}
onPress={() => router.push("/test-notifications")} // onPress={() => router.push("/test-notifications")}
/> // />
), // ),
}} }}
/> /> */}
{/* ========== Event Section ========= */} {/* ========== Event Section ========= */}
<Stack.Screen <Stack.Screen
name="event/(tabs)" name="event/(tabs)"
options={{ options={{
title: "Event", title: "Event",
headerLeft: () => ( // NOTE: DIPINDAH DI FILE /Event/(Tabs)/_layout.tsx
<LeftButtonCustom path="/(application)/(user)/home" /> // headerLeft: () => (
), // <LeftButtonCustom path="/(application)/(user)/home" />
// ),
}} }}
/> />
<Stack.Screen <Stack.Screen
name="event/create" name="event/create"
options={{ options={{
@@ -520,7 +525,7 @@ export default function UserLayout() {
options={{ options={{
title: "Job Vacancy", title: "Job Vacancy",
// headerLeft: () => <BackButton path="/home" />, // headerLeft: () => <BackButton path="/home" />,
// Note: headerLeft di pindahkan ke Tabs Layout // NOTE: headerLeft di pindahkan ke Tabs Layout
}} }}
/> />
<Stack.Screen <Stack.Screen
@@ -611,6 +616,15 @@ export default function UserLayout() {
headerLeft: () => <BackButton />, headerLeft: () => <BackButton />,
}} }}
/> />
<Stack.Screen
name="forum/[id]/preview-report-posting"
options={{
title: "Preview Laporan Diskusi",
headerLeft: () => <BackButton />,
}}
/>
{/* ========== Maps Section ========= */} {/* ========== Maps Section ========= */}
<Stack.Screen <Stack.Screen

View File

@@ -16,6 +16,7 @@ import Donation_ComponentBoxDetailData from "@/screens/Donation/ComponentBoxDeta
import Donation_ComponentStoryFunrising from "@/screens/Donation/ComponentStoryFunrising"; import Donation_ComponentStoryFunrising from "@/screens/Donation/ComponentStoryFunrising";
import Donation_ProgressSection from "@/screens/Donation/ProgressSection"; import Donation_ProgressSection from "@/screens/Donation/ProgressSection";
import { apiDonationGetOne } from "@/service/api-client/api-donation"; import { apiDonationGetOne } from "@/service/api-client/api-donation";
import { countDownAndCondition } from "@/utils/countDownAndCondition";
import { FontAwesome6 } from "@expo/vector-icons"; import { FontAwesome6 } from "@expo/vector-icons";
import { import {
router, router,
@@ -24,7 +25,7 @@ import {
useLocalSearchParams, useLocalSearchParams,
} from "expo-router"; } from "expo-router";
import _ from "lodash"; import _ from "lodash";
import { useCallback, useState } from "react"; import { useCallback, useEffect, useState } from "react";
export default function DonasiDetailStatus() { export default function DonasiDetailStatus() {
const { id, status } = useLocalSearchParams(); const { id, status } = useLocalSearchParams();
@@ -58,6 +59,27 @@ export default function DonasiDetailStatus() {
setOpenDrawer(false); setOpenDrawer(false);
}; };
const [value, setValue] = useState({
sisa: 0,
reminder: false,
});
useEffect(() => {
updateCountDown();
}, [data]);
const updateCountDown = () => {
const countDown = countDownAndCondition({
duration: data?.DonasiMaster_Durasi?.name,
publishTime: data?.publishTime,
});
setValue({
sisa: countDown.durationDay,
reminder: countDown.reminder,
});
};
return ( return (
<> <>
<Stack.Screen <Stack.Screen
@@ -74,10 +96,15 @@ export default function DonasiDetailStatus() {
/> />
<ViewWrapper> <ViewWrapper>
<Donation_ComponentBoxDetailData <Donation_ComponentBoxDetailData
sisaHari={value.sisa}
reminder={value.reminder}
data={data} data={data}
bottomSection={ bottomSection={
status === "publish" && ( status === "publish" && (
<Donation_ProgressSection id={id as string} /> <Donation_ProgressSection
id={id as string}
progres={Number(data?.progres) || 0}
/>
) )
} }
/> />

View File

@@ -4,10 +4,34 @@ import {
IconHome, IconHome,
IconStatus, IconStatus,
} from "@/components/_Icon"; } from "@/components/_Icon";
import BackButtonFromNotification from "@/components/Button/BackButtonFromNotification";
import { TabsStyles } from "@/styles/tabs-styles"; import { TabsStyles } from "@/styles/tabs-styles";
import { Tabs } from "expo-router"; import { router, Tabs, useLocalSearchParams, useNavigation } from "expo-router";
import { useLayoutEffect } from "react";
export default function EventTabsLayout() { export default function EventTabsLayout() {
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 ( return (
<Tabs screenOptions={TabsStyles}> <Tabs screenOptions={TabsStyles}>
<Tabs.Screen <Tabs.Screen

View File

@@ -11,15 +11,17 @@ import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
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 } from "expo-router"; import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash"; import _ from "lodash";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
export default function EventStatus() { export default function EventStatus() {
const { user } = useAuth(); const { user } = useAuth();
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>(
"publish" status || "publish"
); );
const [listData, setListData] = useState([]); const [listData, setListData] = useState([]);
const [loadingGetData, setLoadingGetData] = useState(false); const [loadingGetData, setLoadingGetData] = useState(false);
@@ -73,7 +75,7 @@ export default function EventStatus() {
listData.map((item: any, i) => ( listData.map((item: any, i) => (
<BoxWithHeaderSection <BoxWithHeaderSection
key={i} key={i}
href={`/event/${item.id }/${activeCategory}/detail-event`} href={`/event/${item.id}/${activeCategory}/detail-event`}
> >
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
<Grid> <Grid>

View File

@@ -56,7 +56,7 @@ export default function EventDetailHistory() {
<DrawerCustom <DrawerCustom
isVisible={openDrawer} isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)} closeDrawer={() => setOpenDrawer(false)}
height={250} height={"auto"}
> >
<MenuDrawerDynamicGrid <MenuDrawerDynamicGrid
data={menuDrawerPublishEvent({ id: id as string })} data={menuDrawerPublishEvent({ id: id as string })}

View File

@@ -1,14 +1,15 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { import {
AlertDefaultSystem, AlertDefaultSystem,
BackButton,
ButtonCustom, ButtonCustom,
DotButton, DotButton,
DrawerCustom, DrawerCustom,
LoaderCustom,
MenuDrawerDynamicGrid, MenuDrawerDynamicGrid,
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import { IMenuDrawerItem } from "@/components/_Interface/types"; import { IMenuDrawerItem } from "@/components/_Interface/types";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import LeftButtonCustom from "@/components/Button/BackButton"; import LeftButtonCustom from "@/components/Button/BackButton";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import Event_BoxDetailPublishSection from "@/screens/Event/BoxDetailPublishSection"; import Event_BoxDetailPublishSection from "@/screens/Event/BoxDetailPublishSection";
@@ -18,23 +19,26 @@ import {
apiEventGetOne, apiEventGetOne,
apiEventJoin, apiEventJoin,
} from "@/service/api-client/api-event"; } from "@/service/api-client/api-event";
import dayjs from "dayjs";
import { import {
Redirect,
router, router,
Stack, Stack,
useFocusEffect, useFocusEffect,
useLocalSearchParams, useLocalSearchParams,
} from "expo-router"; } from "expo-router";
import { useCallback, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import Toast from "react-native-toast-message"; import Toast from "react-native-toast-message";
export default function EventDetailPublish() { export default function EventDetailPublish() {
const { id } = useLocalSearchParams(); const now = new Date().toISOString();
const { user } = useAuth(); const { user } = useAuth();
const { id } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = useState(false); const [openDrawer, setOpenDrawer] = useState(false);
const [isLoadingData, setIsLoadingData] = useState(false); const [isLoadingData, setIsLoadingData] = useState(false);
const [isLoadingJoin, setIsLoadingJoin] = useState(false); const [isLoadingJoin, setIsLoadingJoin] = useState(false);
const [data, setData] = useState(); const [data, setData] = useState<any>();
const [isParticipant, setIsParticipant] = useState<boolean | null>(null); const [isParticipant, setIsParticipant] = useState<boolean | null>(null);
useFocusEffect( useFocusEffect(
@@ -55,8 +59,6 @@ export default function EventDetailPublish() {
userId: user?.id as string, userId: user?.id as string,
}); });
console.log("[RES CHECK PARTICIPANTS]", responseCheckParticipants);
if ( if (
responseCheckParticipants.success && responseCheckParticipants.success &&
responseCheckParticipants.data responseCheckParticipants.data
@@ -108,7 +110,24 @@ export default function EventDetailPublish() {
} }
}; };
const footerButton = () => { const isEventFinished =
id && data?.tanggalSelesai && dayjs(data.tanggalSelesai).isBefore(now);
useEffect(() => {
if (isEventFinished) {
router.replace(`/(application)/(user)/event/${id}/history`);
}
}, [isEventFinished, id]);
if (isEventFinished) {
return (
<ViewWrapper>
<CustomSkeleton />
</ViewWrapper>
);
}
const FooterButton = () => {
return ( return (
<> <>
<ButtonCustom <ButtonCustom
@@ -138,17 +157,17 @@ export default function EventDetailPublish() {
<Stack.Screen <Stack.Screen
options={{ options={{
title: `Event Publish`, title: `Event Publish`,
headerLeft: () => <LeftButtonCustom />, headerLeft: () => <BackButton onPress={() => router.back()} />,
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />, headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
}} }}
/> />
<ViewWrapper> <ViewWrapper>
{isLoadingData ? ( {isLoadingData ? (
<LoaderCustom /> <CustomSkeleton height={400} />
) : ( ) : (
<Event_BoxDetailPublishSection <Event_BoxDetailPublishSection
data={data} data={data}
footerButton={footerButton()} footerButton={FooterButton()}
/> />
)} )}
</ViewWrapper> </ViewWrapper>

View File

@@ -14,7 +14,7 @@ import { apiEventCreate } from "@/service/api-client/api-event";
import { apiMasterEventType } from "@/service/api-client/api-master"; import { apiMasterEventType } from "@/service/api-client/api-master";
import { DateTimePickerEvent } from "@react-native-community/datetimepicker"; import { DateTimePickerEvent } from "@react-native-community/datetimepicker";
import { router } from "expo-router"; import { router } from "expo-router";
import React, { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import Toast from "react-native-toast-message"; import Toast from "react-native-toast-message";
interface EventCreateProps { interface EventCreateProps {
@@ -78,23 +78,6 @@ export default function EventCreate() {
return; return;
} }
// if (selectedDate) {
// console.log("Tanggal yang dipilih:", selectedDate);
// console.log(`ISO Format ${Platform.OS}:`, selectedDate.toString());
// // Kirim ke API atau proses lanjutan
// } else {
// console.log("Tanggal belum dipilih");
// }
// if (selectedEndDate) {
// console.log("Tanggal yang dipilih:", selectedEndDate);
// console.log(`ISO Format ${Platform.OS}:`, selectedEndDate.toString());
// // Kirim ke API atau proses lanjutan
// } else {
// console.log("Tanggal berakhir belum dipilih");
// }
try { try {
setIsLoading(true); setIsLoading(true);
@@ -110,7 +93,7 @@ export default function EventCreate() {
const response = await apiEventCreate(newData); const response = await apiEventCreate(newData);
console.log("Response", JSON.stringify(response, null, 2)); console.log("Response", JSON.stringify(response, null, 2));
router.replace("/event/status"); router.replace("/event/status?status=review");
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} finally { } finally {

View File

@@ -19,34 +19,20 @@ import {
apiForumGetOne, apiForumGetOne,
apiForumUpdateStatus, apiForumUpdateStatus,
} from "@/service/api-client/api-forum"; } from "@/service/api-client/api-forum";
import { TypeForum_CommentProps } from "@/types/type-forum";
import { isBadContent } from "@/utils/badWordsIndonesia"; import { isBadContent } from "@/utils/badWordsIndonesia";
import { useFocusEffect, useLocalSearchParams } from "expo-router"; import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash"; import _ from "lodash";
import { useCallback, useEffect, useState } from "react"; 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;
};
};
}
export default function ForumDetail() { export default function ForumDetail() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const { user } = useAuth(); const { user } = useAuth();
const [openDrawer, setOpenDrawer] = useState(false); const [openDrawer, setOpenDrawer] = useState(false);
const [data, setData] = useState<any | null>(null); const [data, setData] = useState<any | null>(null);
const [listComment, setListComment] = useState<CommentProps[] | null>(null); const [listComment, setListComment] = useState<TypeForum_CommentProps[] | null>(null);
const [isLoadingComment, setLoadingComment] = useState(false); const [isLoadingComment, setLoadingComment] = useState(false);
// Status // Status
@@ -122,6 +108,7 @@ export default function ForumDetail() {
comment: text, comment: text,
authorId: user?.id, authorId: user?.id,
}; };
try { try {
setLoadingComment(true); setLoadingComment(true);
const response = await apiForumCreateComment({ const response = await apiForumCreateComment({

View File

@@ -0,0 +1,43 @@
import { NewWrapper, TextCustom } from "@/components";
import { useAuth } from "@/hooks/use-auth";
import {
apiForumGetOne,
apiForumGetReportPosting,
} from "@/service/api-client/api-forum";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
export default function ForumPreviewReportPosting() {
const { id } = useLocalSearchParams();
const { user } = useAuth();
const [data, setData] = useState<any | null>(null);
// Status
useFocusEffect(
useCallback(() => {
onLoadData(id as string);
}, [id])
);
const onLoadData = async (id: string) => {
try {
const response = await apiForumGetReportPosting({ id });
setData(response.data);
} catch (error) {
console.log("[ERROR]", error);
}
};
return (
<>
<NewWrapper>
<TextCustom>Halaman preview repost posting</TextCustom>
<TextCustom>{JSON.stringify(data, null, 2)}</TextCustom>
<TextCustom>untuk report jomentar beda halaman</TextCustom>
</NewWrapper>
</>
);
}

View File

@@ -2,15 +2,14 @@ import {
BoxButtonOnFooter, BoxButtonOnFooter,
ButtonCustom, ButtonCustom,
TextAreaCustom, TextAreaCustom,
ViewWrapper ViewWrapper,
} from "@/components"; } from "@/components";
import AlertWarning from "@/components/Alert/AlertWarning"; import AlertWarning from "@/components/Alert/AlertWarning";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { apiForumCreate } from "@/service/api-client/api-forum"; 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 { router } from "expo-router";
import { useState } from "react"; import { useState } from "react";
import { Alert } from "react-native";
import Toast from "react-native-toast-message"; import Toast from "react-native-toast-message";
export default function ForumCreate() { export default function ForumCreate() {
@@ -19,16 +18,22 @@ export default function ForumCreate() {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const handlerSubmit = async () => { const handlerSubmit = async () => {
if (text.trim() === "") {
if (isBadContent(text)) { AlertWarning({
AlertWarning({}) title: "Lengkapi Data",
description: "Postingan tidak boleh kosong",
});
return; return;
} }
// Bisa di sensor atau return dan tidak bisa di post
const cencorContent = censorText(text)
const newData = { const newData = {
diskusi: text, diskusi: cencorContent,
authorId: user?.id, authorId: user?.id,
}; };
try { try {
setIsLoading(true); setIsLoading(true);
const response = await apiForumCreate({ data: newData }); const response = await apiForumCreate({ data: newData });
@@ -50,6 +55,7 @@ export default function ForumCreate() {
const buttonFooter = ( const buttonFooter = (
<BoxButtonOnFooter> <BoxButtonOnFooter>
<ButtonCustom <ButtonCustom
disabled={!text.trim() || isLoading}
isLoading={isLoading} isLoading={isLoading}
onPress={() => { onPress={() => {
handlerSubmit(); handlerSubmit();

View File

@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { ButtonCustom, StackCustom, ViewWrapper } from "@/components"; import { StackCustom, ViewWrapper } from "@/components";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
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";
@@ -23,14 +23,12 @@ export default function Application() {
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const { syncUnreadCount } = useNotificationStore(); const { syncUnreadCount } = useNotificationStore();
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
onLoadData(); onLoadData();
checkVersion(); checkVersion();
userData(token as string); userData(token as string);
syncUnreadCount() syncUnreadCount();
}, [user?.id, token]) }, [user?.id, token])
); );
@@ -56,10 +54,10 @@ export default function Application() {
setRefreshing(false); setRefreshing(false);
}, []); }, []);
if (user && user?.termsOfServiceAccepted === false) { // if (user && user?.termsOfServiceAccepted === false) {
console.log("User is not accept term service"); // console.log("User is not accept term service");
return <Redirect href={`/terms-agreement`} />; // return <Redirect href={`/terms-agreement`} />;
} // }
if (data && data?.active === false) { if (data && data?.active === false) {
console.log("User is not active"); console.log("User is not active");

View File

@@ -11,9 +11,7 @@ import {
} from "@/components"; } from "@/components";
import NoDataText from "@/components/_ShareComponent/NoDataText"; import NoDataText from "@/components/_ShareComponent/NoDataText";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { import { apiInvestmentGetAll } from "@/service/api-client/api-investment";
apiInvestmentGetAll
} from "@/service/api-client/api-investment";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay"; import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import { router, useFocusEffect } from "expo-router"; import { router, useFocusEffect } from "expo-router";
import _ from "lodash"; import _ from "lodash";
@@ -63,37 +61,20 @@ export default function InvestmentMyHolding() {
router.push(`/investment/${item?.id}/(my-holding)/${item?.id}`) router.push(`/investment/${item?.id}/(my-holding)/${item?.id}`)
} }
> >
<Grid> <StackCustom>
<Grid.Col span={6}> <TextCustom truncate={2}>{item?.title}</TextCustom>
<StackCustom gap={"xs"}> <TextCustom>
<TextCustom truncate={2}>{item?.title}</TextCustom> Rp. {formatCurrencyDisplay(item?.nominal)}
</TextCustom>
<Spacing height={5} /> <TextCustom>{item?.lembarTerbeli} Lembar</TextCustom>
<TextCustom size="small"> <ProgressCustom
Rp. {formatCurrencyDisplay(item?.nominal)} label={`${item.progress}%`}
</TextCustom> value={Number(item.progress)}
<TextCustom size="small"> size="lg"
{item?.lembarTerbeli} Lembar animated
</TextCustom> color="primary"
</StackCustom> />
</Grid.Col> </StackCustom>
<Grid.Col span={1}>
<View />
</Grid.Col>
<Grid.Col
span={5}
style={{
justifyContent: "center",
alignItems: "center",
}}
>
<ProgressCustom
value={item?.progress}
label={`${item?.progress}%`}
size="lg"
/>
</Grid.Col>
</Grid>
</BaseBox> </BaseBox>
)) ))
)} )}

View File

@@ -15,6 +15,7 @@ import Investment_ButtonInvestasiSection from "@/screens/Invesment/ButtonInvesta
import Invesment_ComponentBoxOnBottomDetail from "@/screens/Invesment/ComponentBoxOnBottomDetail"; import Invesment_ComponentBoxOnBottomDetail from "@/screens/Invesment/ComponentBoxOnBottomDetail";
import Invesment_DetailDataPublishSection from "@/screens/Invesment/DetailDataPublishSection"; import Invesment_DetailDataPublishSection from "@/screens/Invesment/DetailDataPublishSection";
import { apiInvestmentGetOne } from "@/service/api-client/api-investment"; import { apiInvestmentGetOne } from "@/service/api-client/api-investment";
import { countDownAndCondition } from "@/utils/countDownAndCondition";
import { AntDesign, MaterialIcons } from "@expo/vector-icons"; import { AntDesign, MaterialIcons } from "@expo/vector-icons";
import { import {
router, router,
@@ -23,7 +24,7 @@ import {
useLocalSearchParams, useLocalSearchParams,
} from "expo-router"; } from "expo-router";
import _ from "lodash"; import _ from "lodash";
import { useCallback, useState } from "react"; import { useCallback, useEffect, useState } from "react";
export default function InvestmentDetailStatus() { export default function InvestmentDetailStatus() {
const { user } = useAuth(); const { user } = useAuth();
@@ -63,6 +64,29 @@ export default function InvestmentDetailStatus() {
setOpenDrawerPublish(false); setOpenDrawerPublish(false);
}; };
const [value, setValue] = useState({
sisa: 0,
reminder: false,
});
useEffect(() => {
updateCountDown();
}, [data]);
console.log("[DATA DETAIL]", JSON.stringify(data, null, 2));
const updateCountDown = () => {
const countDown = countDownAndCondition({
duration: data?.MasterPencarianInvestor.name,
publishTime: data?.countDown,
});
setValue({
sisa: countDown.durationDay,
reminder: countDown.reminder,
});
};
const bottomSection = ( const bottomSection = (
<Invesment_ComponentBoxOnBottomDetail <Invesment_ComponentBoxOnBottomDetail
id={data?.id} id={data?.id}
@@ -72,7 +96,11 @@ export default function InvestmentDetailStatus() {
); );
const buttonSection = ( const buttonSection = (
<Investment_ButtonInvestasiSection id={id as string} isMine={user?.id === data?.author?.id} /> <Investment_ButtonInvestasiSection
id={id as string}
isMine={user?.id === data?.author?.id}
reminder={value.reminder}
/>
); );
return ( return (

View File

@@ -63,30 +63,28 @@ export default function InvestmentDetail() {
setOpenDrawerPublish(false); setOpenDrawerPublish(false);
}; };
const [value, setValue] = useState({ const [value, setValue] = useState({
sisa: 0, sisa: 0,
reminder: false, reminder: false,
});
useEffect(() => {
updateCountDown();
}, [data]);
console.log("[DATA DETAIL]", JSON.stringify(data, null, 2));
const updateCountDown = () => {
const countDown = countDownAndCondition({
duration: data?.MasterPencarianInvestor.name,
publishTime: data?.countDown,
}); });
useEffect(() => {
updateCountDown();
}, [data]);
console.log("[DATA DETAIL]", JSON.stringify(data, null, 2));
const updateCountDown = () => {
const countDown = countDownAndCondition({
duration: data?.MasterPencarianInvestor.name,
publishTime: data?.countDown,
});
setValue({
sisa: countDown.durationDay,
reminder: countDown.reminder,
});
};
setValue({
sisa: countDown.durationDay,
reminder: countDown.reminder,
});
};
const bottomSection = ( const bottomSection = (
<Invesment_ComponentBoxOnBottomDetail <Invesment_ComponentBoxOnBottomDetail
@@ -97,7 +95,11 @@ export default function InvestmentDetail() {
); );
const buttonSection = ( const buttonSection = (
<Investment_ButtonInvestasiSection id={id as string} isMine={user?.id === data?.author?.id} reminder={value.reminder} /> <Investment_ButtonInvestasiSection
id={id as string}
isMine={user?.id === data?.author?.id}
reminder={value.reminder}
/>
); );
return ( return (

View File

@@ -1,6 +1,7 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { BackButton } from "@/components"; import { BackButton } from "@/components";
import { IconHome, IconStatus } from "@/components/_Icon"; import { IconHome, IconStatus } from "@/components/_Icon";
import BackButtonFromNotification from "@/components/Button/BackButtonFromNotification";
import { TabsStyles } from "@/styles/tabs-styles"; import { TabsStyles } from "@/styles/tabs-styles";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { import {
@@ -23,19 +24,7 @@ export default function JobTabsLayout() {
useLayoutEffect(() => { useLayoutEffect(() => {
navigation.setOptions({ navigation.setOptions({
headerLeft: () => ( headerLeft: () => (
<BackButton <BackButtonFromNotification from={from as string} category={category as string} />
onPress={() => {
if (from === "notifications") {
router.replace(`/notifications?category=${category}`);
} else {
if (from) {
router.replace(`/${from}` as any);
} else {
router.navigate("/home");
}
}
}}
/>
), ),
}); });
}, [from, router, navigation]); }, [from, router, navigation]);

View File

@@ -1,20 +1,27 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { import {
AlertDefaultSystem,
BackButton,
BaseBox, BaseBox,
DrawerCustom,
MenuDrawerDynamicGrid,
NewWrapper, NewWrapper,
ScrollableCustom, ScrollableCustom,
StackCustom, StackCustom,
TextCustom, TextCustom,
} from "@/components"; } from "@/components";
import { IconDot } from "@/components/_Icon/IconComponent";
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent"; import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
import NoDataText from "@/components/_ShareComponent/NoDataText"; import NoDataText from "@/components/_ShareComponent/NoDataText";
import { AccentColor } from "@/constants/color-palet"; import { AccentColor, MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
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";
import { apiGetNotificationsById } from "@/service/api-notifications"; import { apiGetNotificationsById } from "@/service/api-notifications";
import { listOfcategoriesAppNotification } from "@/types/type-notification-category"; import { listOfcategoriesAppNotification } from "@/types/type-notification-category";
import { formatChatTime } from "@/utils/formatChatTime"; import { formatChatTime } from "@/utils/formatChatTime";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; import { Ionicons } from "@expo/vector-icons";
import { router, Stack, useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash"; import _ from "lodash";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { RefreshControl, View } from "react-native"; import { RefreshControl, View } from "react-native";
@@ -39,8 +46,9 @@ const fixPath = ({
const separator = deepLink.includes("?") ? "&" : "?"; const separator = deepLink.includes("?") ? "&" : "?";
const fixedPath = const fixedPath = `${deepLink}${separator}from=notifications&category=${_.lowerCase(
`${deepLink}${separator}from=notifications&category=${_.lowerCase(categoryApp)}`; categoryApp
)}`;
console.log("Fix Path", fixedPath); console.log("Fix Path", fixedPath);
@@ -70,7 +78,7 @@ const BoxNotification = ({
categoryApp: data.kategoriApp, categoryApp: data.kategoriApp,
}); });
router.replace(newPath as any); router.navigate(newPath as any);
selectedCategory(activeCategory as string); selectedCategory(activeCategory as string);
if (!data.isRead) { if (!data.isRead) {
@@ -103,6 +111,9 @@ export default function Notifications() {
const [listData, setListData] = useState<any[]>([]); const [listData, setListData] = useState<any[]>([]);
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [openDrawer, setOpenDrawer] = useState(false);
const { markAsReadAll } = useNotificationStore();
const handlePress = (item: any) => { const handlePress = (item: any) => {
setActiveCategory(item.value); setActiveCategory(item.value);
@@ -142,33 +153,96 @@ export default function Notifications() {
}; };
return ( return (
<NewWrapper <>
headerComponent={ <Stack.Screen
<ScrollableCustom options={{
data={listOfcategoriesAppNotification.map((e, i) => ({ title: "Notifikasi",
id: i, headerLeft: () => <BackButton />,
label: e.label, headerRight: () => (
value: e.value, <IconDot
}))} color={MainColor.yellow}
onButtonPress={handlePress} onPress={() => setOpenDrawer(true)}
activeId={activeCategory as string} />
),
}}
/>
<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>
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>
); );
} }

View File

@@ -1,13 +1,37 @@
import { import {
IconContribution, IconContribution,
IconHistory, IconHistory,
IconHome, IconHome,
IconStatus, IconStatus,
} from "@/components/_Icon"; } from "@/components/_Icon";
import BackButtonFromNotification from "@/components/Button/BackButtonFromNotification";
import { TabsStyles } from "@/styles/tabs-styles"; import { TabsStyles } from "@/styles/tabs-styles";
import { Tabs } from "expo-router"; import { Tabs, useLocalSearchParams, useNavigation, router } from "expo-router";
import { useLayoutEffect } from "react";
export default function VotingTabsLayout() { export default function VotingTabsLayout() {
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 ( return (
<Tabs screenOptions={TabsStyles}> <Tabs screenOptions={TabsStyles}>
<Tabs.Screen <Tabs.Screen

View File

@@ -12,15 +12,17 @@ import { useAuth } from "@/hooks/use-auth";
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status"; import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
import { apiVotingGetByStatus } from "@/service/api-client/api-voting"; import { apiVotingGetByStatus } from "@/service/api-client/api-voting";
import { dateTimeView } from "@/utils/dateTimeView"; import { dateTimeView } from "@/utils/dateTimeView";
import { useFocusEffect } from "expo-router"; import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash"; import _ from "lodash";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
export default function VotingStatus() { export default function VotingStatus() {
const { user } = useAuth(); const { user } = useAuth();
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>(
"publish" status || "publish"
); );
const [listData, setListData] = useState([]); const [listData, setListData] = useState([]);
@@ -86,8 +88,14 @@ export default function VotingStatus() {
style={{ width: "70%", alignSelf: "center" }} style={{ width: "70%", alignSelf: "center" }}
variant="light" variant="light"
> >
{item?.awalVote && dateTimeView({date: item?.awalVote, withoutTime: true})} -{" "} {item?.awalVote &&
{item?.akhirVote && dateTimeView({date: item?.akhirVote, withoutTime: true})} dateTimeView({
date: item?.awalVote,
withoutTime: true,
})}{" "}
-{" "}
{item?.akhirVote &&
dateTimeView({ date: item?.akhirVote, withoutTime: true })}
</BadgeCustom> </BadgeCustom>
</StackCustom> </StackCustom>
</BaseBox> </BaseBox>

View File

@@ -13,6 +13,7 @@ import {
} from "@/components"; } from "@/components";
import { IconArchive, IconContribution } from "@/components/_Icon"; import { IconArchive, IconContribution } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types"; import { IMenuDrawerItem } from "@/components/_Interface/types";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import Voting_BoxDetailHasilVotingSection from "@/screens/Voting/BoxDetailHasilVotingSection"; import Voting_BoxDetailHasilVotingSection from "@/screens/Voting/BoxDetailHasilVotingSection";
import { Voting_BoxDetailPublishSection } from "@/screens/Voting/BoxDetailPublishSection"; import { Voting_BoxDetailPublishSection } from "@/screens/Voting/BoxDetailPublishSection";
@@ -22,13 +23,14 @@ import {
apiVotingUpdateData, apiVotingUpdateData,
} from "@/service/api-client/api-voting"; } from "@/service/api-client/api-voting";
import { today } from "@/utils/dateTimeView"; import { today } from "@/utils/dateTimeView";
import dayjs from "dayjs";
import { import {
router, router,
Stack, Stack,
useFocusEffect, useFocusEffect,
useLocalSearchParams, useLocalSearchParams,
} from "expo-router"; } from "expo-router";
import React, { useCallback, useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import Toast from "react-native-toast-message"; import Toast from "react-native-toast-message";
export default function VotingDetail() { export default function VotingDetail() {
@@ -119,6 +121,23 @@ export default function VotingDetail() {
setOpenDrawerPublish(false); setOpenDrawerPublish(false);
}; };
const now = new Date().toISOString();
const isEventFinished = id && data?.akhirVote && dayjs(data.akhirVote).isBefore(now);
useEffect(() => {
if (isEventFinished) {
router.replace(`/(application)/(user)/voting/${id}/history`);
}
}, [isEventFinished, id]);
if (isEventFinished) {
return (
<ViewWrapper>
<CustomSkeleton />
</ViewWrapper>
);
}
return ( return (
<> <>
<Stack.Screen <Stack.Screen

View File

@@ -79,7 +79,7 @@ export default function VotingCreate() {
type: "success", type: "success",
text1: "Data berhasil disimpan", text1: "Data berhasil disimpan",
}); });
router.replace("/(application)/(user)/voting/(tabs)/status"); router.replace("/(application)/(user)/voting/(tabs)/status?status=review");
} else { } else {
Toast.show({ Toast.show({
type: "error", type: "error",

View File

@@ -41,8 +41,8 @@ export default function AdminEventDetail() {
const deepLinkURL = `${DEEP_LINK_URL}/event/${id}/confirmation?userId=${user?.id}`; const deepLinkURL = `${DEEP_LINK_URL}/event/${id}/confirmation?userId=${user?.id}`;
const deepLinkURLDEV = `${DEEP_LINK_URL}/--/event/${id}/confirmation?userId=${user?.id}`; const deepLinkURLDEV = `${DEEP_LINK_URL}/--/event/${id}/confirmation?userId=${user?.id}`;
const isDevLink = process.env.NODE_ENV === "development" ? deepLinkURLDEV : deepLinkURL; const isDevLink =
process.env.NODE_ENV === "development" ? deepLinkURLDEV : deepLinkURL;
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
@@ -126,6 +126,7 @@ export default function AdminEventDetail() {
const response = await funUpdateStatusEvent({ const response = await funUpdateStatusEvent({
id: id as string, id: id as string,
changeStatus: "publish", changeStatus: "publish",
data: { catatan: "", senderId: user?.id as string },
}); });
if (!response.success) { if (!response.success) {

View File

@@ -7,6 +7,7 @@ import {
} from "@/components"; } from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle"; import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject"; import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
import { useAuth } from "@/hooks/use-auth";
import { funUpdateStatusEvent } from "@/screens/Admin/Event/funUpdateStatus"; import { funUpdateStatusEvent } from "@/screens/Admin/Event/funUpdateStatus";
import { apiAdminEventById } from "@/service/api-admin/api-admin-event"; import { apiAdminEventById } from "@/service/api-admin/api-admin-event";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
@@ -14,9 +15,13 @@ import { useCallback, useState } from "react";
import Toast from "react-native-toast-message"; import Toast from "react-native-toast-message";
export default function AdminEventRejectInput() { export default function AdminEventRejectInput() {
const { user } = useAuth();
const { id, status } = useLocalSearchParams(); const { id, status } = useLocalSearchParams();
const [data, setData] = useState<any>(""); const [data, setData] = useState<any>({
catatan: "",
senderId: "",
});
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
useFocusEffect( useFocusEffect(
@@ -45,10 +50,16 @@ export default function AdminEventRejectInput() {
}) => { }) => {
try { try {
setIsLoading(true); setIsLoading(true);
const newData = {
catatan: data,
senderId: user?.id as string,
};
const response = await funUpdateStatusEvent({ const response = await funUpdateStatusEvent({
id: id as string, id: id as string,
changeStatus, changeStatus,
data: data, data: newData,
}); });
if (!response.success) { if (!response.success) {

View File

@@ -16,12 +16,11 @@ import { IconDot, IconView } from "@/components/_Icon/IconComponent";
import { IconTrash } from "@/components/_Icon/IconTrash"; import { IconTrash } from "@/components/_Icon/IconTrash";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle"; import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage"; 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_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent"; import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value"; import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";
import { import {
apiAdminForumDeactivatePosting, apiAdminForumDeactivatePosting,
apiAdminForumListReportPostingById, apiAdminForumListReportPostingById,
@@ -35,6 +34,7 @@ import { Divider } from "react-native-paper";
import Toast from "react-native-toast-message"; import Toast from "react-native-toast-message";
export default function AdminForumReportPosting() { export default function AdminForumReportPosting() {
const { user } = useAuth();
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const [openDrawerPage, setOpenDrawerPage] = useState(false); const [openDrawerPage, setOpenDrawerPage] = useState(false);
const [openDrawerAction, setOpenDrawerAction] = useState(false); const [openDrawerAction, setOpenDrawerAction] = useState(false);
@@ -215,6 +215,9 @@ export default function AdminForumReportPosting() {
onPressRight: async () => { onPressRight: async () => {
const response = await apiAdminForumDeactivatePosting({ const response = await apiAdminForumDeactivatePosting({
id: id as string, id: id as string,
data: {
senderId: user?.id as string,
},
}); });
if (!response.success) { if (!response.success) {

View File

@@ -1,20 +1,26 @@
import { import {
AlertDefaultSystem,
BackButton, BackButton,
BaseBox, BaseBox,
DrawerCustom,
MenuDrawerDynamicGrid,
NewWrapper, NewWrapper,
ScrollableCustom, ScrollableCustom,
StackCustom, StackCustom,
TextCustom, TextCustom,
} from "@/components"; } from "@/components";
import { IconPlus } from "@/components/_Icon"; import { IconPlus } from "@/components/_Icon";
import { IconDot } from "@/components/_Icon/IconComponent";
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent"; import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
import NoDataText from "@/components/_ShareComponent/NoDataText"; import NoDataText from "@/components/_ShareComponent/NoDataText";
import { AccentColor, MainColor } from "@/constants/color-palet"; import { AccentColor, MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
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";
import { apiGetNotificationsById } from "@/service/api-notifications"; import { apiGetNotificationsById } from "@/service/api-notifications";
import { listOfcategoriesAppNotification } from "@/types/type-notification-category"; import { listOfcategoriesAppNotification } from "@/types/type-notification-category";
import { formatChatTime } from "@/utils/formatChatTime"; import { formatChatTime } from "@/utils/formatChatTime";
import { Ionicons } from "@expo/vector-icons";
import { router, Stack, useFocusEffect } from "expo-router"; import { router, Stack, useFocusEffect } from "expo-router";
import _ from "lodash"; import _ from "lodash";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
@@ -70,6 +76,9 @@ export default function AdminNotification() {
const [listData, setListData] = useState<any[]>([]); const [listData, setListData] = useState<any[]>([]);
const [refreshing, setRefreshing] = useState(false); const [refreshing, setRefreshing] = useState(false);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [openDrawer, setOpenDrawer] = useState(false);
const { markAsReadAll } = useNotificationStore();
const handlePress = (item: any) => { const handlePress = (item: any) => {
setActiveCategory(item.value); setActiveCategory(item.value);
@@ -89,7 +98,7 @@ export default function AdminNotification() {
id: user?.id as any, id: user?.id as any,
category: activeCategory as any, category: activeCategory as any,
}); });
if (response.success) { if (response.success) {
setListData(response.data); setListData(response.data);
} else { } else {
@@ -115,9 +124,9 @@ export default function AdminNotification() {
title: "Admin Notifikasi", title: "Admin Notifikasi",
headerLeft: () => <BackButton />, headerLeft: () => <BackButton />,
headerRight: () => ( headerRight: () => (
<IconPlus <IconDot
color={MainColor.yellow} color={MainColor.yellow}
onPress={() => router.push("/test-notifications")} onPress={() => setOpenDrawer(true)}
/> />
), ),
}} }}
@@ -154,6 +163,51 @@ export default function AdminNotification() {
)) ))
)} )}
</NewWrapper> </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>
</> </>
); );
} }

View File

@@ -16,6 +16,7 @@ import AdminButtonReview from "@/components/_ShareComponent/Admin/ButtonReview";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8"; import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import ReportBox from "@/components/Box/ReportBox"; import ReportBox from "@/components/Box/ReportBox";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import funUpdateStatusVoting from "@/screens/Admin/Voting/funUpdateStatus"; import funUpdateStatusVoting from "@/screens/Admin/Voting/funUpdateStatus";
import { apiAdminVotingById } from "@/service/api-admin/api-admin-voting"; import { apiAdminVotingById } from "@/service/api-admin/api-admin-voting";
import { colorBadgeStatus } from "@/utils/colorBadge"; import { colorBadgeStatus } from "@/utils/colorBadge";
@@ -29,6 +30,7 @@ import { List } from "react-native-paper";
import Toast from "react-native-toast-message"; import Toast from "react-native-toast-message";
export default function AdminVotingDetail() { export default function AdminVotingDetail() {
const { user } = useAuth();
const { id, status } = useLocalSearchParams(); const { id, status } = useLocalSearchParams();
const [data, setData] = useState<any | null>(null); const [data, setData] = useState<any | null>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -139,6 +141,10 @@ export default function AdminVotingDetail() {
const response = await funUpdateStatusVoting({ const response = await funUpdateStatusVoting({
id: id as string, id: id as string,
changeStatus, changeStatus,
data: {
senderId: user?.id as string,
catatan: "",
},
}); });
if (!response.success) { if (!response.success) {

View File

@@ -7,6 +7,7 @@ import {
} from "@/components"; } from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle"; import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject"; import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
import { useAuth } from "@/hooks/use-auth";
import funUpdateStatusVoting from "@/screens/Admin/Voting/funUpdateStatus"; import funUpdateStatusVoting from "@/screens/Admin/Voting/funUpdateStatus";
import { apiAdminVotingById } from "@/service/api-admin/api-admin-voting"; import { apiAdminVotingById } from "@/service/api-admin/api-admin-voting";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
@@ -14,6 +15,7 @@ import { useCallback, useState } from "react";
import Toast from "react-native-toast-message"; import Toast from "react-native-toast-message";
export default function AdminVotingRejectInput() { export default function AdminVotingRejectInput() {
const { user } = useAuth()
const { id, status } = useLocalSearchParams(); const { id, status } = useLocalSearchParams();
const [data, setData] = useState(""); const [data, setData] = useState("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -48,7 +50,10 @@ export default function AdminVotingRejectInput() {
const response = await funUpdateStatusVoting({ const response = await funUpdateStatusVoting({
id: id as string, id: id as string,
changeStatus, changeStatus,
data: data, data: {
catatan: data,
senderId: user?.id as string,
},
}); });
if (!response.success) { if (!response.success) {

9
app/eula.tsx Normal file
View File

@@ -0,0 +1,9 @@
import EULAView from "@/screens/Authentication/EULAView";
export default function EULA() {
return (
<>
<EULAView />
</>
);
}

View File

@@ -0,0 +1,29 @@
import { useRouter } from "expo-router";
import { BackButton } from "..";
export default function BackButtonFromNotification({
from,
category,
}: {
from: string;
category?: string;
}) {
const router = useRouter();
return (
<>
<BackButton
onPress={() => {
if (from === "notifications") {
router.replace(`/notifications?category=${category}`);
} else {
if (from) {
router.replace(`/${from}` as any);
} else {
router.navigate("/home");
}
}
}}
/>
</>
);
}

View File

@@ -24,8 +24,6 @@ const DateTimePickerCustom: React.FC<Props> = ({
disabled = false, disabled = false,
}) => { }) => {
console.log("Date Android Comp", value)
return ( return (
<> <>
{Platform.OS === "ios" ? ( {Platform.OS === "ios" ? (

View File

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

View File

@@ -2,6 +2,7 @@ import {
apiConfig, apiConfig,
apiLogin, apiLogin,
apiRegister, apiRegister,
apiUpdatedTermCondition,
apiValidationCode, apiValidationCode,
} from "@/service/api-config"; } from "@/service/api-config";
import { apiDeviceTokenDeleted } from "@/service/api-device-token"; import { apiDeviceTokenDeleted } from "@/service/api-device-token";
@@ -29,6 +30,7 @@ type AuthContextType = {
termsOfServiceAccepted: boolean; termsOfServiceAccepted: boolean;
}) => Promise<void>; }) => Promise<void>;
userData: (token: string) => Promise<any>; userData: (token: string) => Promise<any>;
acceptedTerms: (nomor: string) => Promise<any>;
}; };
// --- Create Context --- // --- Create Context ---
@@ -74,28 +76,32 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const loginWithNomor = async (nomor: string) => { const loginWithNomor = async (nomor: string) => {
setIsLoading(true); setIsLoading(true);
try { try {
console.log("[Masuk provider]", nomor);
const response = await apiLogin({ nomor: nomor }); const response = await apiLogin({ nomor: nomor });
console.log("[RESPONSE AUTH]", JSON.stringify(response)); console.log("[RESPONSE AUTH]", JSON.stringify(response, null, 2));
if (response.success) { if (response.success) {
console.log("[Keluar provider]", nomor); if (response.isAcceptTerms) {
Toast.show({ Toast.show({
type: "success", type: "success",
text1: "Sukses", text1: "Sukses",
text2: "Kode OTP berhasil dikirim", text2: "Kode OTP berhasil dikirim",
}); });
await AsyncStorage.setItem("kode_otp", response.kodeId); await AsyncStorage.setItem("kode_otp", response.kodeId);
router.push(`/verification?nomor=${nomor}`); router.push(`/verification?nomor=${nomor}`);
return; return;
} else {
router.push(`/eula?nomor=${nomor}`);
return;
}
} else { } else {
router.push(`/register?nomor=${nomor}`); router.push(`/eula?nomor=${nomor}`);
Toast.show({
type: "info", // Toast.show({
text1: "Info", // type: "info",
text2: "Silahkan mendaftar", // text1: "Info",
}); // text2: "Silahkan mendaftar",
// });
return; return;
} }
} catch (error: any) { } catch (error: any) {
@@ -226,7 +232,6 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
setIsLoading(false); setIsLoading(false);
} }
}; };
// --- 5. Logout --- // --- 5. Logout ---
@@ -256,6 +261,31 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
} }
}; };
const acceptedTerms = async (nomor: string) => {
try {
setIsLoading(true);
const response = await apiUpdatedTermCondition({ nomor: nomor });
if (response.success) {
router.replace(`/verification?nomor=${nomor}`);
} else {
if (response.status === 404) {
router.replace(`/register?nomor=${nomor}`);
} else {
Toast.show({
type: "error",
text1: "Error",
text2: response.message,
});
}
}
} catch (error) {
console.log("Error accept terms", error);
} finally {
setIsLoading(false);
}
};
return ( return (
<> <>
<AuthContext.Provider <AuthContext.Provider
@@ -271,6 +301,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
logout, logout,
registerUser, registerUser,
userData, userData,
acceptedTerms,
}} }}
> >
{children} {children}

View File

@@ -11,7 +11,7 @@
"preview": { "preview": {
"distribution": "internal", "distribution": "internal",
"android": { "android": {
"buildType": "app-bundle" "buildType": "apk"
}, },
"ios": { "ios": {
"simulator": false "simulator": false

View File

@@ -13,3 +13,13 @@ Exp: open ios/HIPMIBADUNG.xcworkspace
perubahan versi : npm version patch perubahan versi : npm version patch
ios: bunx expo prebuild --platform ios ios: bunx expo prebuild --platform ios
android: bunx expo prebuild --platform android android: bunx expo prebuild --platform android
### 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
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

View File

@@ -4,6 +4,7 @@ import {
onMessage, onMessage,
FirebaseMessagingTypes, FirebaseMessagingTypes,
} from "@react-native-firebase/messaging"; } from "@react-native-firebase/messaging";
import { useAuth } from "./use-auth";
// Gunakan tipe resmi dari library // Gunakan tipe resmi dari library
type RemoteMessage = FirebaseMessagingTypes.RemoteMessage; type RemoteMessage = FirebaseMessagingTypes.RemoteMessage;
@@ -11,17 +12,26 @@ type RemoteMessage = FirebaseMessagingTypes.RemoteMessage;
export function useForegroundNotifications( export function useForegroundNotifications(
onMessageReceived: (message: RemoteMessage) => void onMessageReceived: (message: RemoteMessage) => void
) { ) {
const { user } = useAuth();
useEffect(() => { useEffect(() => {
const messaging = getMessaging(); const messaging = getMessaging();
const unsubscribe = onMessage(messaging, (remoteMessage) => { const unsubscribe = onMessage(messaging, (remoteMessage) => {
const data = remoteMessage.data;
// console.log("DATA NOTIFIKASI DARI SERVER", data)
if (data?.recipientId && data?.recipientId !== user?.id) {
console.log("📵 Notification untuk user lain", data);
return;
}
console.log( console.log(
"🔔 Notifikasi diterima saat app aktif:", "🔔 Notifikasi diterima saat app aktif:",
JSON.stringify(remoteMessage, null, 2) JSON.stringify(data, null, 2)
); );
onMessageReceived(remoteMessage); onMessageReceived(remoteMessage);
}); });
return unsubscribe; return unsubscribe;
}, [onMessageReceived]); }, [user?.id, onMessageReceived]);
} }

View File

@@ -40,6 +40,7 @@ type NotificationContextType = {
notif: Omit<AppNotification, "id" | "isRead" | "timestamp"> notif: Omit<AppNotification, "id" | "isRead" | "timestamp">
) => void; ) => void;
markAsRead: (id: string) => void; markAsRead: (id: string) => void;
markAsReadAll: (id: string) => void;
syncUnreadCount: () => Promise<void>; syncUnreadCount: () => Promise<void>;
}; };
@@ -48,6 +49,7 @@ const NotificationContext = createContext<NotificationContextType>({
unreadCount: 0, unreadCount: 0,
addNotification: () => {}, addNotification: () => {},
markAsRead: () => {}, markAsRead: () => {},
markAsReadAll: () => {},
syncUnreadCount: async () => {}, syncUnreadCount: async () => {},
}); });
@@ -98,7 +100,7 @@ export const NotificationProvider = ({ children }: { children: ReactNode }) => {
const markAsRead = async (id: string) => { const markAsRead = async (id: string) => {
try { try {
const response = await apiNotificationMarkAsRead({ id }); const response = await apiNotificationMarkAsRead({ id, category: "one" });
console.log("🚀 Response Mark As Read:", response); console.log("🚀 Response Mark As Read:", response);
if (response.success) { if (response.success) {
@@ -114,6 +116,25 @@ export const NotificationProvider = ({ children }: { children: ReactNode }) => {
} }
}; };
const markAsReadAll = async (id: string) => {
try {
const response = await apiNotificationMarkAsRead({ id, category: "all" });
console.log("🚀 Response Mark As Read All:", response);
if (response.success) {
const cloneNotifications = [...notifications];
const index = cloneNotifications.findIndex((n) => n?.data?.id === id);
if (index !== -1) {
cloneNotifications[index].isRead = true;
setNotifications(cloneNotifications);
}
}
} catch (error) {
console.error("Gagal mark as read:", error);
}
};
const syncUnreadCount = async () => { const syncUnreadCount = async () => {
try { try {
const count = await apiNotificationUnreadCount({ const count = await apiNotificationUnreadCount({
@@ -133,8 +154,9 @@ export const NotificationProvider = ({ children }: { children: ReactNode }) => {
value={{ value={{
notifications, notifications,
addNotification, addNotification,
markAsRead,
unreadCount, unreadCount,
markAsRead,
markAsReadAll,
syncUnreadCount, syncUnreadCount,
}} }}
> >

View File

@@ -458,7 +458,7 @@
); );
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = "com.anonymous.hipmi-mobile"; PRODUCT_BUNDLE_IDENTIFIER = "com.anonymous.hipmi-mobile";
PRODUCT_NAME = HIPMIBadungConnect; PRODUCT_NAME = "HIPMIBadungConnect";
SWIFT_OBJC_BRIDGING_HEADER = "HIPMIBadungConnect/HIPMIBadungConnect-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "HIPMIBadungConnect/HIPMIBadungConnect-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@@ -490,7 +490,7 @@
); );
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE"; OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = "com.anonymous.hipmi-mobile"; PRODUCT_BUNDLE_IDENTIFIER = "com.anonymous.hipmi-mobile";
PRODUCT_NAME = HIPMIBadungConnect; PRODUCT_NAME = "HIPMIBadungConnect";
SWIFT_OBJC_BRIDGING_HEADER = "HIPMIBadungConnect/HIPMIBadungConnect-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "HIPMIBadungConnect/HIPMIBadungConnect-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2"; TARGETED_DEVICE_FAMILY = "1,2";

View File

@@ -39,7 +39,7 @@
</dict> </dict>
</array> </array>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>17</string> <string>19</string>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false/> <false/>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>

View File

@@ -1,4 +1,5 @@
import { apiAdminEventUpdateStatus } from "@/service/api-admin/api-admin-event"; import { apiAdminEventUpdateStatus } from "@/service/api-admin/api-admin-event";
import { typeRejectedData } from "@/types/type-collect-other";
export const funUpdateStatusEvent = async ({ export const funUpdateStatusEvent = async ({
id, id,
@@ -7,17 +8,19 @@ export const funUpdateStatusEvent = async ({
}: { }: {
id: string; id: string;
changeStatus: "publish" | "review" | "reject"; changeStatus: "publish" | "review" | "reject";
data?: string; data?: typeRejectedData;
}) => { }) => {
try { try {
console.log("[DATA]", data);
const response = await apiAdminEventUpdateStatus({ const response = await apiAdminEventUpdateStatus({
id: id, id: id,
changeStatus: changeStatus as any, changeStatus: changeStatus as any,
data: data, data: data as any,
}); });
return response; return response;
} catch (error) { } catch (error) {
console.log("[ERROR]", error); console.log("[ERROR]", error);
throw error; throw error;
} }
}; };

View File

@@ -1,4 +1,5 @@
import { apiAdminVotingUpdateStatus } from "@/service/api-admin/api-admin-voting"; import { apiAdminVotingUpdateStatus } from "@/service/api-admin/api-admin-voting";
import { typeRejectedData } from "@/types/type-collect-other";
const funUpdateStatusVoting = async ({ const funUpdateStatusVoting = async ({
id, id,
@@ -7,7 +8,7 @@ const funUpdateStatusVoting = async ({
}: { }: {
id: string; id: string;
changeStatus: "publish" | "review" | "reject"; changeStatus: "publish" | "review" | "reject";
data?: string; data?: typeRejectedData;
}) => { }) => {
try { try {
const response = await apiAdminVotingUpdateStatus({ const response = await apiAdminVotingUpdateStatus({

View File

@@ -0,0 +1,278 @@
// app/syarat-dan-ketentuan.tsx
import {
View,
Text,
ScrollView,
TouchableOpacity,
StyleSheet,
} from "react-native";
import { useState, useRef } from "react";
import { 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";
// 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();
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 () => {
try {
if (!isAtBottom) return;
setIsLoading(true);
await acceptedTerms(nomor as string);
} catch (error) {
console.log("Error accept terms", error);
} 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",
},
buttonText: {
color: "#fff",
fontSize: 16,
fontWeight: "600",
},
});

View File

@@ -1,4 +1,3 @@
import { CheckboxCustom } from "@/components";
import Spacing from "@/components/_ShareComponent/Spacing"; import Spacing from "@/components/_ShareComponent/Spacing";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper"; import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import ButtonCustom from "@/components/Button/ButtonCustom"; import ButtonCustom from "@/components/Button/ButtonCustom";
@@ -7,7 +6,6 @@ import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { BASE_URL } from "@/service/api-config"; import { BASE_URL } from "@/service/api-config";
import { GStyles } from "@/styles/global-styles"; import { GStyles } from "@/styles/global-styles";
import { openBrowser } from "@/utils/openBrower";
import { MaterialCommunityIcons } from "@expo/vector-icons"; import { MaterialCommunityIcons } from "@expo/vector-icons";
import { useLocalSearchParams } from "expo-router"; import { useLocalSearchParams } from "expo-router";
import { useState } from "react"; import { useState } from "react";
@@ -17,7 +15,7 @@ import Toast from "react-native-toast-message";
export default function RegisterView() { export default function RegisterView() {
const { nomor } = useLocalSearchParams(); const { nomor } = useLocalSearchParams();
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [term, setTerm] = useState(false); // const [term, setTerm] = useState(false);
const url = BASE_URL; const url = BASE_URL;
const { registerUser, isLoading } = useAuth(); const { registerUser, isLoading } = useAuth();
@@ -65,7 +63,7 @@ export default function RegisterView() {
await registerUser({ await registerUser({
nomor: nomor as string, nomor: nomor as string,
username: usernameLower, username: usernameLower,
termsOfServiceAccepted: term, termsOfServiceAccepted: true,
}); });
} }
@@ -106,12 +104,12 @@ export default function RegisterView() {
flexDirection: "row", flexDirection: "row",
alignItems: "center", alignItems: "center",
marginTop: 16, marginTop: 16,
marginBottom: 16, // marginBottom: 16,
}} }}
> >
<CheckboxCustom value={term} onChange={() => setTerm(!term)} /> {/* <CheckboxCustom value={term} onChange={() => setTerm(!term)} /> */}
<Text style={GStyles.textLabel}> {/* <Text style={GStyles.textLabel}>
Saya setuju dengan{" "} Saya setuju dengan{" "}
<Text <Text
style={{ style={{
@@ -126,11 +124,11 @@ export default function RegisterView() {
Syarat & Ketentuan Syarat & Ketentuan
</Text>{" "} </Text>{" "}
yang melarang konten tidak pantas dan perilaku merugikan. yang melarang konten tidak pantas dan perilaku merugikan.
</Text> </Text> */}
</View> </View>
<ButtonCustom <ButtonCustom
disabled={!term} disabled={isLoading}
isLoading={isLoading} isLoading={isLoading}
onPress={handleRegister} onPress={handleRegister}
> >

View File

@@ -13,7 +13,8 @@ export const tabsHome: any = ({
icon: "chatbubble-ellipses-outline", icon: "chatbubble-ellipses-outline",
activeIcon: "chatbubble-ellipses", activeIcon: "chatbubble-ellipses",
label: "Forum", label: "Forum",
path: acceptedForumTermsAt ? "/forum" : "/forum/terms", // path: acceptedForumTermsAt ? "/forum" : "/forum/terms",
path: "/forum",
isActive: true, isActive: true,
disabled: false, disabled: false,
}, },

View File

@@ -1,16 +1,28 @@
import { BaseBox, StackCustom, TextCustom, ProgressCustom } from "@/components"; import { BaseBox, StackCustom, TextCustom, ProgressCustom } from "@/components";
export default function Invesment_BoxProgressSection({progress, status}: {progress: number, status: string}) { export default function Invesment_BoxProgressSection({
return ( progress,
<> status,
{status === "publish" && ( }: {
<BaseBox> progress: number;
<StackCustom> status: string;
<TextCustom bold>Progress Saham</TextCustom> }) {
<ProgressCustom label={(progress || 0) + "%"} value={progress || 0} size="lg" /> return (
</StackCustom> <>
</BaseBox> {status === "publish" && (
)} <BaseBox>
</> <StackCustom>
); <TextCustom bold>Progress Saham</TextCustom>
} <ProgressCustom
label={(progress || 0) + "%"}
value={progress || 0}
size="lg"
animated
color="primary"
/>
</StackCustom>
</BaseBox>
)}
</>
);
}

View File

@@ -15,6 +15,7 @@ export default function AppRoot() {
name="index" name="index"
options={{ title: "", headerBackVisible: false }} options={{ title: "", headerBackVisible: false }}
/> />
<Stack.Screen name="eula" options={{ title: "Terms & Conditions", headerBackVisible: false }} />
<Stack.Screen name="+not-found" options={{ title: "" }} /> <Stack.Screen name="+not-found" options={{ title: "" }} />
<Stack.Screen <Stack.Screen
name="verification" name="verification"

View File

@@ -81,9 +81,17 @@ export async function apiAdminForumListReportPostingById({
} }
} }
export async function apiAdminForumDeactivatePosting({ id }: { id: string }) { export async function apiAdminForumDeactivatePosting({
id,
data,
}: {
id: string;
data: { senderId: string };
}) {
try { try {
const response = await apiConfig.put(`/mobile/admin/forum/${id}`); const response = await apiConfig.put(`/mobile/admin/forum/${id}`, {
data: data,
});
return response.data; return response.data;
} catch (error) { } catch (error) {
throw error; throw error;

View File

@@ -1,3 +1,4 @@
import { typeRejectedData } from "@/types/type-collect-other";
import { apiConfig } from "../api-config"; import { apiConfig } from "../api-config";
export async function apiAdminVoting({ export async function apiAdminVoting({
@@ -32,7 +33,7 @@ export async function apiAdminVotingUpdateStatus({
status, status,
}: { }: {
id: string; id: string;
data?: string; data?: typeRejectedData;
status: "publish" | "review" | "reject"; status: "publish" | "review" | "reject";
}) { }) {
try { try {

View File

@@ -119,3 +119,12 @@ export async function apiForumDeleteComment({ id }: { id: string }) {
throw error; throw error;
} }
} }
export async function apiForumGetReportPosting({id}: {id:string}) {
try {
const response = await apiConfig.get(`/mobile/forum/${id}/preview-report-posting`);
return response.data;
} catch (error) {
throw error;
}
}

View File

@@ -74,3 +74,10 @@ export async function apiAcceptTermService({
return response.data; return response.data;
} }
export async function apiUpdatedTermCondition({nomor}: {nomor: string}) {
const response = await apiConfig.post(`/auth/mobile-eula`, {
nomor: nomor,
});
return response.data;
}

View File

@@ -1,6 +1,6 @@
import { import {
NotificationProp, NotificationProp,
TypeNotificationCategoryApp TypeNotificationCategoryApp,
} from "@/types/type-notification-category"; } from "@/types/type-notification-category";
import { apiConfig } from "./api-config"; import { apiConfig } from "./api-config";
@@ -78,9 +78,22 @@ export async function apiNotificationUnreadCount({
} }
} }
export async function apiNotificationMarkAsRead({ id }: { id: string }) { /**
* @param id | notification id atau user id
* @param category | "all" | "one" , jika "all" id yang harus di masukan adalah user id, jika "one" id yang harus di masukan adalah notification id
* @type {string}
*/
export async function apiNotificationMarkAsRead({
id,
category,
}: {
id: string;
category: "all" | "one";
}) {
try { try {
const response = await apiConfig.put(`/mobile/notification/${id}`); const response = await apiConfig.put(
`/mobile/notification/${id}?category=${category}`
);
return response.data; return response.data;
} catch (error) { } catch (error) {
throw error; throw error;

View File

@@ -0,0 +1,4 @@
export type typeRejectedData = {
catatan?: string;
senderId: string;
};

15
types/type-forum.ts Normal file
View File

@@ -0,0 +1,15 @@
export interface TypeForum_CommentProps {
id: string;
isActive: boolean;
komentar: string;
createdAt: Date;
authorId: string;
Author: {
id: string;
username: string;
Profile: {
id: string;
imageId: string;
};
};
}

View File

@@ -15,21 +15,21 @@ export const formatChatTime = (date: string | Date): string => {
const messageDate = dayjs(date); const messageDate = dayjs(date);
const now = dayjs(); const now = dayjs();
// Jika hari ini // Hari ini
if (messageDate.isSame(now, 'day')) { if (messageDate.isSame(now, 'day')) {
return messageDate.format('HH:mm'); // contoh: "14.30" return messageDate.format('HH.mm'); // "14.30"
} }
// Jika kemarin // Kemarin
if (messageDate.isSame(now.subtract(1, 'day'), 'day')) { if (messageDate.isSame(now.subtract(1, 'day'), 'day')) {
return messageDate.format('dddd HH:mm'); return `Kemarin, ${messageDate.format('HH.mm')}`; // "Kemarin, 14.30"
} }
// Jika dalam 7 hari terakhir (tapi bukan kemarin/ hari ini) // Dalam 7 hari terakhir (termasuk hari ini & kemarin sudah di-handle, jadi aman)
if (now.diff(messageDate, 'day') < 7) { if (now.diff(messageDate, 'day') < 7) {
return messageDate.format('dddd HH:mm'); // contoh: "Senin 14:30" return `${messageDate.format('dddd')}, ${messageDate.format('HH.mm')}`; // "Senin, 13.00"
} }
// Lebih dari seminggu lalu → tampilkan tanggal // Lebih dari 7 hari lalu
return messageDate.format('D MMM YYYY HH:mm'); // contoh: "12 Mei 2024 14:30" return messageDate.format('DD - MM - YYYY, HH.mm'); // "05 - 11 - 2025, 14.00"
}; };