Compare commits

..

16 Commits

Author SHA1 Message Date
6f9481c7c9 Fix QC ( Ayu )
Fix:
- modified:   app/(application)/(user)/event/[id]/publish.tsx
- modified:   app/(application)/(user)/event/create.tsx
- modified:   app/(application)/(user)/portofolio/[id]/create.tsx
- modified:   app/(application)/(user)/portofolio/[id]/edit.tsx
- modified:   app/(application)/admin/collaboration/[id]/group.tsx
- modified:   app/(application)/admin/collaboration/group.tsx
- modified:   app/(application)/admin/collaboration/publish.tsx
- modified:   app/(application)/admin/forum/[id]/list-report-comment.tsx
- modified:   app/(application)/admin/forum/[id]/list-report-posting.tsx
- modified:   app/(application)/admin/forum/posting.tsx
- modified:   app/(application)/admin/forum/report-comment.tsx
- modified:   app/(application)/admin/forum/report-posting.tsx
- modified:   app/(application)/admin/voting/[status]/status.tsx
- modified:   app/(application)/admin/voting/history.tsx
- modified:   components/Select/SelectCustom.tsx
- modified:   components/_ShareComponent/GridSpan_4_8.tsx
- modified:   screens/Authentication/LoginView.tsx
- modified:   screens/Collaboration/BoxPublishSection.tsx
- modified:   screens/Event/BoxDetailPublishSection.tsx
- modified:   screens/Home/topFeatureSection.tsx
- modified:   screens/Portofolio/ButtonCreatePortofolio.tsx

Add:
- app/(application)/admin/app-information/business-field/[id]/bidang-update.tsx
- app/(application)/admin/app-information/business-field/[id]/sub-bidang-update.tsx

### No Issue
2025-12-10 17:35:15 +08:00
cccb44a835 Fix QC Ayu
Fix:
- modified:   app/(application)/(user)/event/[id]/publish.tsx
- modified:   app/(application)/(user)/event/create.tsx
- modified:   app/(application)/(user)/portofolio/[id]/create.tsx
- modified:   app/(application)/(user)/portofolio/[id]/edit.tsx
- modified:   app/(application)/admin/collaboration/[id]/group.tsx
- modified:   app/(application)/admin/collaboration/group.tsx
- modified:   app/(application)/admin/collaboration/publish.tsx
- modified:   app/(application)/admin/forum/[id]/list-report-comment.tsx
- modified:   app/(application)/admin/forum/[id]/list-report-posting.tsx
- modified:   app/(application)/admin/forum/posting.tsx
- modified:   app/(application)/admin/forum/report-comment.tsx
- modified:   app/(application)/admin/forum/report-posting.tsx
- modified:   app/(application)/admin/voting/[status]/status.tsx
- modified:   app/(application)/admin/voting/history.tsx
- modified:   components/Select/SelectCustom.tsx
- modified:   components/_ShareComponent/GridSpan_4_8.tsx
- modified:   screens/Authentication/LoginView.tsx
- modified:   screens/Collaboration/BoxPublishSection.tsx
- modified:   screens/Event/BoxDetailPublishSection.tsx
- modified:   screens/Home/topFeatureSection.tsx
- modified:   screens/Portofolio/ButtonCreatePortofolio.tsx

Add:
- components/_ShareComponent/GridSpan_NewComponent.tsx

### No Issue
2025-12-09 17:36:36 +08:00
0f5862ce70 Fix Apple Reject:
Add:
- app/(application)/(user)/forum/terms.tsx

Fix:
- app/(application)/(user)/_layout.tsx
- app/(application)/(user)/home.tsx
- screens/Home/tabsList.ts
- service/api-client/api-user.ts

### No Issue
2025-12-08 16:34:33 +08:00
624bd49f69 QC Admin ( Inno )
Fix:
   modified:   android/app/build.gradle
        modified:   app.config.js
        modified:   app/(application)/admin/donation/[id]/[status]/index.tsx
        modified:   app/(application)/admin/donation/[id]/[status]/transaction-detail.tsx
        modified:   app/(application)/admin/donation/[id]/detail-disbursement-of-funds.tsx
        modified:   app/(application)/admin/donation/category.tsx
        modified:   app/(application)/admin/event/[id]/[status]/index.tsx
        modified:   app/(application)/admin/event/[id]/list-of-participants.tsx
        modified:   app/(application)/admin/event/[status]/status.tsx
        modified:   app/(application)/admin/forum/[id]/index.tsx
        modified:   app/(application)/admin/forum/[id]/list-report-comment.tsx
        modified:   app/(application)/admin/forum/[id]/list-report-posting.tsx
        modified:   app/(application)/admin/investment/[id]/[status]/index.tsx
        modified:   app/(application)/admin/investment/[id]/[status]/transaction-detail.tsx
        modified:   app/(application)/admin/voting/[id]/[status]/index.tsx
        modified:   components/DateInput/DateTimeIOS.tsx
        modified:   components/_ShareComponent/Admin/ButtonReject.tsx
        deleted:    components/_ShareComponent/GridDetail_4_8.tsx

Add:/
components/_ShareComponent/GridSpan_4_8.tsx

### No Issue
2025-12-05 17:20:39 +08:00
2446e9d51a Fix apple reject EULA
Add:
components/Alert/AlertWarning.ts
        utils/badWordsIndonesia.ts

Fix:
- app.config.js
- app/(application)/(user)/forum/[id]/edit.tsx
- app/(application)/(user)/forum/[id]/index.tsx
- app/(application)/(user)/forum/create.tsx
- ios/HIPMIBadungConnect/Info.plist

### No Issue
2025-12-05 11:46:36 +08:00
ab5733f336 Fix redirect admin 2025-12-04 17:41:19 +08:00
f5e30087ed Fix QC Inno
Fix:
- app/(application)/admin/donation/category-create.tsx
- app/(application)/admin/donation/category-update.tsx
- app/(application)/admin/donation/category.tsx
- components/_ShareComponent/Admin/TableValue.tsx
- screens/Authentication/LoginView.tsx
- service/api-admin/api-master-admin.ts

### No Issue
2025-12-04 16:59:39 +08:00
a93f97ed6a Fix rejected Apple
Add:
-  utils/viersionBadge.ts

Fix:
- app.config.js
- context/AuthContext.tsx
- ios/HIPMIBadungConnect/Info.plist
- screens/Authentication/LoginView.tsx
- screens/Authentication/VerificationView.tsx
- service/api-config.ts

### No Issue
2025-12-03 17:23:12 +08:00
858b441a8c Clearing apple rejected
QC: Inno

Fix:
- app.config.js
- app/(application)/(user)/investment/[id]/index.tsx
- app/(application)/(user)/voting/(tabs)/index.tsx
- app/(application)/(user)/waiting-room.tsx
- app/(application)/terms-agreement.tsx
- context/AuthContext.tsx
- ios/HIPMIBadungConnect.xcodeproj/project.pbxproj
- ios/HIPMIBadungConnect/Info.plist
- screens/Authentication/LoginView.tsx
- screens/Authentication/VerificationView.tsx
- screens/Home/topFeatureSection.tsx
- screens/Invesment/BoxBerandaSection.tsx
- screens/Invesment/ButtonInvestasiSection.tsx
- screens/Invesment/DetailDataPublishSection.tsx
- service/api-client/api-voting.ts
- service/api-config.ts

### No Issue
2025-12-02 17:48:24 +08:00
98aaa126a1 QC: Inno dan Pak Jun
Fix:
- app/(application)/(user)/collaboration/create.tsx
- app/(application)/(user)/event/[id]/edit.tsx
- app/(application)/(user)/event/create.tsx
- app/(application)/(user)/profile/[id]/blocked-list.tsx
- app/(application)/(user)/profile/[id]/index.tsx
- app/(application)/(user)/voting/[id]/[status]/detail.tsx
- components/Button/FloatingButton.tsx
- components/TextArea/TextAreaCustom.tsx
- components/TextInput/TextInputCustom.tsx
- constants/color-palet.ts
- screens/Authentication/LoginView.tsx
- screens/Home/topFeatureSection.tsx
- screens/Portofolio/SocialMediaSection.tsx
- screens/Voting/BoxDetailHasilVotingSection.tsx
- styles/global-styles.ts

### No Issue
2025-12-01 17:43:20 +08:00
69452ff4e7 Fix loader fetch data di Forum dan Forumku
### No Issue
2025-11-28 17:27:59 +08:00
33ec892ec8 Prebuild : untuk Maps box
### No issue
2025-11-28 16:35:18 +08:00
8a900e9469 Halaman unblock user
Add:
- app/(application)/(user)/profile/[id]/blocked-list.tsx
app/(application)/(user)/profile/[id]/detail-blocked.tsx
components/_ShareComponent/ListEmptyComponent.tsx
components/_ShareComponent/ListLoaderFooterComponent.tsx
components/_ShareComponent/ListSkeletonComponent.tsx
hooks/use-paginated-api.ts
service/api-client/api-blocked.ts

Fix:
modified:   app/(application)/(user)/profile/_layout.tsx
modified:   components/_ShareComponent/NewWrapper.tsx
modified:   components/index.ts
modified:   screens/Profile/ListPage.tsx
modified:   styles/global-styles.ts

### No Issue
2025-11-28 13:55:48 +08:00
d471682ae7 Refresh control dan Blockir user di forum
### No Issue
2025-11-26 16:13:05 +08:00
00eea71248 Penambahan fitur block user: 50%
Fix:
- app/(application)/(user)/forum/[id]/index.tsx
- app/(application)/(user)/home.tsx
- screens/Forum/ListPage.tsx
- screens/Forum/MenuDrawerSection.tsx/MenuBeranda.tsx
- service/api-client/api-master.ts
- service/api-client/api-user.ts

### No Issue
2025-11-25 11:04:12 +08:00
41e648d8f3 Fix rejected Apple :
Submission ID: 1efcd8eb-7d68-4348-9925-43a8e1bd7d1e

Add:
-  app/(application)/terms-agreement.tsx

Fix:
- app/(application)/(user)/home.tsx
- app/(application)/_layout.tsx
- context/AuthContext.tsx
- ios/HIPMIBadungConnect/Info.plist
- screens/Authentication/LoginView.tsx
- screens/Authentication/RegisterView.tsx
- service/api-config.ts
- types/User.ts

### NO Issue
2025-11-24 17:09:52 +08:00
115 changed files with 3944 additions and 985 deletions

View File

@@ -82,6 +82,14 @@ def enableMinifyInReleaseBuilds = (findProperty('android.enableMinifyInReleaseBu
def jscFlavor = 'io.github.react-native-community:jsc-android:2026004.+'
android {
// @generated begin @rnmapbox/maps-libcpp - expo prebuild (DO NOT MODIFY) sync-e24830a5a3e854b398227dfe9630aabfaa1cadd1
packagingOptions {
pickFirst 'lib/x86/libc++_shared.so'
pickFirst 'lib/x86_64/libc++_shared.so'
pickFirst 'lib/arm64-v8a/libc++_shared.so'
pickFirst 'lib/armeabi-v7a/libc++_shared.so'
}
// @generated end @rnmapbox/maps-libcpp
ndkVersion rootProject.ext.ndkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
@@ -92,7 +100,7 @@ android {
applicationId 'com.bip.hipmimobileapp'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 2
versionCode 3
versionName "1.0.1"
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""

View File

@@ -23,3 +23,25 @@ allprojects {
apply plugin: "expo-root-project"
apply plugin: "com.facebook.react.rootproject"
// @generated begin @rnmapbox/maps-v2-maven - expo prebuild (DO NOT MODIFY) sync-d4ccbfdff48fdba3138b02a8ba41b9722af001d8
allprojects {
repositories {
maven {
url 'https://api.mapbox.com/downloads/v2/releases/maven'
// Authentication is no longer required as per Mapbox's removal of download token requirement
// See: https://github.com/mapbox/mapbox-maps-flutter/issues/775
// Keeping this as optional for backward compatibility
def token = project.properties['MAPBOX_DOWNLOADS_TOKEN'] ?: System.getenv('RNMAPBOX_MAPS_DOWNLOAD_TOKEN')
if (token) {
authentication { basic(BasicAuthentication) }
credentials {
username = 'mapbox'
password = token
}
}
}
}
}
// @generated end @rnmapbox/maps-v2-maven

View File

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

View File

@@ -595,6 +595,13 @@ export default function UserLayout() {
headerLeft: () => <BackButton />,
}}
/>
<Stack.Screen
name="forum/terms"
options={{
title: "Syarat & Ketentuan Forum",
headerLeft: () => <BackButton />,
}}
/>
{/* ========== Maps Section ========= */}
<Stack.Screen

View File

@@ -155,7 +155,7 @@ export default function CollaborationCreate() {
<TextAreaCustom
required
label="Keuntungan Proyek"
placeholder="Masukan keuntungan proyek"
placeholder="Masukan keuntungan proyek, contoh: Meningkatkan relasi bisnis , menjamin kualitas produk, meningkatkan kinerja dan lain lain"
showCount
maxLength={1000}
value={data?.benefit}

View File

@@ -18,7 +18,7 @@ import {
import { apiMasterEventType } from "@/service/api-client/api-master";
import { DateTimePickerEvent } from "@react-native-community/datetimepicker";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import React, { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import Toast from "react-native-toast-message";
export default function EventEdit() {
@@ -55,6 +55,7 @@ export default function EventEdit() {
try {
setIsLoadData(true);
const response = await apiEventGetOne({ id: id as string });
console.log("[DATA BY ID]", JSON.stringify(response, null, 2));
if (response.success) {
setData(response.data);
setSelectedDate(new Date(response.data.tanggal));
@@ -209,7 +210,7 @@ export default function EventEdit() {
minimumDate={new Date(Date.now())}
label="Tanggal & Waktu Mulai"
required
value={selectedDate as any}
value={selectedDate}
onChange={(date: any) => {
setSelectedDate(date as any);
}}
@@ -254,7 +255,6 @@ export default function EventEdit() {
placeholder="Masukkan deskripsi event"
required
showCount
maxLength={100}
value={data?.deskripsi}
onChangeText={(value) => setData({ ...data, deskripsi: value })}
/>

View File

@@ -71,8 +71,6 @@ export default function EventDetailPublish() {
}
}
console.log("[participans]", isParticipant);
const handlePress = (item: IMenuDrawerItem) => {
console.log("PATH ", item.path);
router.navigate(item.path as any);
@@ -139,7 +137,7 @@ export default function EventDetailPublish() {
<>
<Stack.Screen
options={{
title: `Event publish`,
title: `Event Publish`,
headerLeft: () => <LeftButtonCustom />,
headerRight: () => <DotButton onPress={() => setOpenDrawer(true)} />,
}}

View File

@@ -110,13 +110,14 @@ export default function EventCreate() {
const response = await apiEventCreate(newData);
console.log("Response", JSON.stringify(response, null, 2));
router.navigate("/event/status");
router.replace("/event/status");
} catch (error) {
console.log(error);
} finally {
setIsLoading(false);
}
};
const buttonSubmit = (
<ButtonCustom
@@ -144,7 +145,7 @@ export default function EventCreate() {
label: item.name,
value: item.id,
}))}
value={data?.eventMaster_TipeAcaraId || ""}
value={data?.eventMaster_TipeAcaraId || null}
onChange={(value: any) =>
setData({ ...data, eventMaster_TipeAcaraId: value })
}
@@ -191,7 +192,7 @@ export default function EventCreate() {
placeholder="Masukkan deskripsi event"
required
showCount
maxLength={1000}
value={data?.deskripsi || ""}
onChangeText={(value: any) =>
setData({ ...data, deskripsi: value })
}

View File

@@ -5,9 +5,12 @@ import {
TextAreaCustom,
ViewWrapper,
} from "@/components";
import AlertWarning from "@/components/Alert/AlertWarning";
import { apiForumGetOne, apiForumUpdate } from "@/service/api-client/api-forum";
import { isBadContent } from "@/utils/badWordsIndonesia";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import { Alert } from "react-native";
import Toast from "react-native-toast-message";
export default function ForumEdit() {
@@ -43,6 +46,12 @@ export default function ForumEdit() {
});
return;
}
if (isBadContent(text)) {
AlertWarning({});
return;
}
try {
setIsLoading(true);
const response = await apiForumUpdate({

View File

@@ -1,142 +1,12 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AvatarComp,
ButtonCustom,
CenterCustom,
DrawerCustom,
FloatingButton,
Grid,
LoaderCustom,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection";
import Forum_MenuDrawerBerandaSection from "@/screens/Forum/MenuDrawerSection.tsx/MenuBeranda";
import { apiForumGetAll } from "@/service/api-client/api-forum";
import { apiUser } from "@/service/api-client/api-user";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import View_Forumku from "@/screens/Forum/ViewForumku";
import View_Forumku2 from "@/screens/Forum/ViewForumku2";
export default function Forumku() {
const { id } = useLocalSearchParams();
const { user } = useAuth();
const [openDrawer, setOpenDrawer] = useState(false);
const [status, setStatus] = useState("");
const [listData, setListData] = useState<any | null>(null);
const [dataUser, setDataUser] = useState<any | null>(null);
const [loadingGetList, setLoadingGetList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
onLoadDataProfile(id as string);
}, [id])
);
const onLoadDataProfile = async (id: string) => {
try {
const response = await apiUser(id);
setDataUser(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
}
};
const onLoadData = async () => {
try {
setLoadingGetList(true);
const response = await apiForumGetAll({
search: "",
authorId: id as string,
});
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetList(false);
}
};
return (
<>
<ViewWrapper
floatingButton={
user?.id === id && (
<FloatingButton
onPress={() =>
router.navigate("/(application)/(user)/forum/create")
}
/>
)
}
>
<StackCustom>
<CenterCustom>
<AvatarComp
fileId={dataUser?.Profile?.imageId}
href={`/(application)/(image)/preview-image/${dataUser?.Profile?.imageId}`}
size="xl"
/>
</CenterCustom>
<Grid>
<Grid.Col span={6}>
<TextCustom bold truncate>
@{dataUser?.username || "-"}
</TextCustom>
<TextCustom>{listData?.length || "0"} postingan</TextCustom>
</Grid.Col>
<Grid.Col span={6} style={{ alignItems: "flex-end" }}>
<ButtonCustom href={`/profile/${dataUser?.Profile?.id}`}>
Kunjungi Profile
</ButtonCustom>
</Grid.Col>
</Grid>
{loadingGetList ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom> Tidak ada diskusi</TextCustom>
) : (
<>
{listData?.map((item: any, index: number) => (
<Forum_BoxDetailSection
isRightComponent={false}
key={index}
data={item}
isTruncate={true}
href={`/forum/${item.id}`}
onSetData={(value) => {
setOpenDrawer(value.setOpenDrawer);
setStatus(value.setStatus);
}}
/>
))}
</>
)}
</StackCustom>
</ViewWrapper>
{/* Drawer Komponen Eksternal */}
<DrawerCustom
height={"auto"}
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
>
<Forum_MenuDrawerBerandaSection
id={id as string}
status={status}
setIsDrawerOpen={() => {
setOpenDrawer(false);
}}
authorId={id as string}
/>
</DrawerCustom>
{/* <View_Forumku /> */}
<View_Forumku2 />
</>
);
}

View File

@@ -7,6 +7,7 @@ import {
TextCustom,
ViewWrapper,
} from "@/components";
import AlertWarning from "@/components/Alert/AlertWarning";
import { useAuth } from "@/hooks/use-auth";
import Forum_CommentarBoxSection from "@/screens/Forum/CommentarBoxSection";
import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection";
@@ -18,9 +19,11 @@ import {
apiForumGetOne,
apiForumUpdateStatus,
} from "@/service/api-client/api-forum";
import { isBadContent } from "@/utils/badWordsIndonesia";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useEffect, useState } from "react";
import { Alert } from "react-native";
interface CommentProps {
id: string;
@@ -110,11 +113,15 @@ export default function ForumDetail() {
// Create Commentar
const handlerCreateCommentar = async () => {
if (isBadContent(text)) {
AlertWarning({});
return;
}
const newData = {
comment: text,
authorId: user?.id,
};
try {
setLoadingComment(true);
const response = await apiForumCreateComment({
@@ -223,6 +230,7 @@ export default function ForumDetail() {
>
<Forum_MenuDrawerBerandaSection
id={dataId}
authorUsername={data?.Author?.username as string}
status={status}
setIsDrawerOpen={() => {
setOpenDrawer(false);

View File

@@ -2,12 +2,15 @@ import {
BoxButtonOnFooter,
ButtonCustom,
TextAreaCustom,
ViewWrapper,
ViewWrapper
} from "@/components";
import AlertWarning from "@/components/Alert/AlertWarning";
import { useAuth } from "@/hooks/use-auth";
import { apiForumCreate } from "@/service/api-client/api-forum";
import { isBadContent } from "@/utils/badWordsIndonesia";
import { router } from "expo-router";
import { useState } from "react";
import { Alert } from "react-native";
import Toast from "react-native-toast-message";
export default function ForumCreate() {
@@ -16,11 +19,16 @@ export default function ForumCreate() {
const [isLoading, setIsLoading] = useState(false);
const handlerSubmit = async () => {
if (isBadContent(text)) {
AlertWarning({})
return;
}
const newData = {
diskusi: text,
authorId: user?.id,
};
try {
setIsLoading(true);
const response = await apiForumCreate({ data: newData });

View File

@@ -1,129 +1,12 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AvatarComp,
BackButton,
DrawerCustom,
LoaderCustom,
SearchInput,
TextCustom,
ViewWrapper,
} from "@/components";
import FloatingButton from "@/components/Button/FloatingButton";
import { useAuth } from "@/hooks/use-auth";
import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection";
import Forum_MenuDrawerBerandaSection from "@/screens/Forum/MenuDrawerSection.tsx/MenuBeranda";
import { apiForumGetAll } from "@/service/api-client/api-forum";
import { apiUser } from "@/service/api-client/api-user";
import { router, Stack, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import Forum_ViewBeranda from "@/screens/Forum/ViewBeranda";
import Forum_ViewBeranda2 from "@/screens/Forum/ViewBeranda2";
export default function Forum() {
const [openDrawer, setOpenDrawer] = useState(false);
const [status, setStatus] = useState("");
const { user } = useAuth();
const [dataUser, setDataUser] = useState<any>();
const [listData, setListData] = useState<any[]>();
const [loadingGetList, setLoadingGetList] = useState(false);
const [search, setSearch] = useState("");
const [dataId, setDataId] = useState("");
const [authorId, setAuthorId] = useState("");
useFocusEffect(
useCallback(() => {
onLoadData();
onLoadDataProfile(user?.id as string);
}, [user?.id, search])
);
const onLoadDataProfile = async (id: string) => {
const response = await apiUser(id);
setDataUser(response.data);
};
const onLoadData = async () => {
try {
setLoadingGetList(true);
const response = await apiForumGetAll({ search: search });
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetList(false);
}
};
return (
<>
<Stack.Screen
options={{
title: "Forum",
headerLeft: () => <BackButton />,
headerRight: () => (
<AvatarComp
fileId={dataUser?.Profile?.imageId}
size="base"
href={`/forum/${user?.id}/forumku`}
/>
),
}}
/>
<ViewWrapper
headerComponent={
<SearchInput
placeholder="Cari topik diskusi"
onChangeText={(e) => setSearch(e)}
/>
}
floatingButton={
<FloatingButton
onPress={() =>
router.navigate("/(application)/(user)/forum/create")
}
/>
}
>
{loadingGetList ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center" color="gray">
Tidak ada diskusi
</TextCustom>
) : (
listData?.map((e: any, i: number) => (
<Forum_BoxDetailSection
key={i}
data={e}
onSetData={() => {
setDataId(e.id);
setOpenDrawer(true);
setStatus(e.ForumMaster_StatusPosting?.status);
setAuthorId(e.Author?.id);
}}
isTruncate={true}
href={`/forum/${e.id}`}
isRightComponent={false}
/>
))
)}
</ViewWrapper>
<DrawerCustom
height={"auto"}
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
>
<Forum_MenuDrawerBerandaSection
id={dataId}
authorId={authorId}
status={status}
setIsDrawerOpen={() => {
setOpenDrawer(false);
}}
/>
</DrawerCustom>
{/* <Forum_ViewBeranda /> */}
<Forum_ViewBeranda2 />
</>
);
}

View File

@@ -0,0 +1,202 @@
import {
BaseBox,
ButtonCustom,
CheckboxCustom,
NewWrapper,
StackCustom,
TextCustom,
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { apiAcceptForumTerms } from "@/service/api-client/api-user";
import { GStyles } from "@/styles/global-styles";
import { Ionicons } from "@expo/vector-icons";
import { router } from "expo-router";
import { useState } from "react";
import { View } from "react-native";
import { Text } from "react-native-paper";
import Toast from "react-native-toast-message";
export default function ForumSplash() {
const { user } = useAuth();
const [term, setTerm] = useState(false);
const [loading, setLoading] = useState(false);
const handleSubmit = async () => {
try {
setLoading(true);
const respone = await apiAcceptForumTerms({
category: "Forum",
userId: user?.id as any,
});
if (respone.success) {
Toast.show({
type: "success",
text1: "Berhasil",
text2: "Terima kasih telah menerima syarat & ketentuan forum ini",
});
router.replace("/(application)/forum");
return;
}
Toast.show({
type: "error",
text1: "Gagal",
text2: "Terjadi kesalahan, silahkan coba lagi",
});
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoading(false);
}
};
return (
<NewWrapper>
{/* <TextCustom bold>HIPMI Badung Connect</TextCustom> . */}
<BaseBox>
<StackCustom>
<TextCustom>
Dengan mengakses dan menggunakan Forum HIPMI Badung Connect, Anda
secara sadar menyetujui ketentuan berikut:
</TextCustom>
<TextCustom bold>
1. Dilarang keras memposting konten yang mengandung:
</TextCustom>
<View style={{ paddingInline: 10 }}>
{forumTerms1.map((term, index) => (
<View
key={index}
style={{
flexDirection: "row",
alignItems: "center",
gap: 10,
paddingBottom: 10,
}}
>
<Ionicons name="radio-button-on" color={"white"} />
<TextCustom>{term.text}</TextCustom>
</View>
))}
</View>
<TextCustom bold>
2. Setiap pengguna bertanggung jawab penuh atas konten yang
diunggah. Konten yang melanggar ketentuan ini dapat dihapus kapan
saja tanpa pemberitahuan.
</TextCustom>
<TextCustom bold>
3. Jika Anda menemukan konten tidak pantas, segera:
</TextCustom>
<View style={{ paddingInline: 10 }}>
{forumTerms2.map((term, index) => (
<View
key={index}
style={{
flexDirection: "row",
alignItems: "center",
gap: 10,
paddingBottom: 10,
}}
>
<Ionicons name="radio-button-on" color={"white"} />
<TextCustom>{term.text}</TextCustom>
</View>
))}
</View>
<TextCustom bold>
4. Gunakan fitur Blokir Pengguna di profil pengguna terkait
</TextCustom>
<View style={{ paddingInline: 10 }}>
{forumTerms3.map((term, index) => (
<View
key={index}
style={{
flexDirection: "row",
alignItems: "center",
gap: 10,
paddingBottom: 10,
}}
>
<Ionicons name="radio-button-on" color={"white"} />
<TextCustom>{term.text}</TextCustom>
</View>
))}
</View>
<TextCustom>
Pelanggaran terhadap ketentuan ini berakibat{" "}
<TextCustom bold>pencabutan akses</TextCustom> ke Forum dan/atau{" "}
<TextCustom bold>pemblokiran akun secara permanen</TextCustom> tanpa
pemberitahuan lebih lanjut.
</TextCustom>
<View
style={{
flexDirection: "row",
alignItems: "center",
marginTop: 16,
marginBottom: 16,
}}
>
<CheckboxCustom value={term} onChange={() => setTerm(!term)} />
<Text style={GStyles.textLabel}>
Saya telah membaca dan menyetujui Syarat & Ketentuan Forum ini
</Text>
</View>
<ButtonCustom
disabled={!term || loading}
onPress={() => {
handleSubmit();
}}
>
Lanjut
</ButtonCustom>
</StackCustom>
</BaseBox>
</NewWrapper>
);
}
// Data dalam format JSON (bisa juga diimpor dari file terpisah)
interface Term {
text: string;
}
const forumTerms1: Term[] = [
{
text: "Ujaran kebencian, diskriminasi, atau konten SARA (Suku, Agama, Ras, Antar-golongan)",
},
{ text: "Kata kasar, pelecehan, ancaman, atau bullying" },
{ text: "Pornografi, hoaks, spam, atau informasi menyesatkan" },
{ text: "Promosi aktivitas ilegal seperti perjudian atau narkoba" },
];
const forumTerms2: Term[] = [
{
text: "Gunakan tombol “Laporkan” di setiap postingan, atau",
},
{
text: "Gunakan fitur “Blokir Pengguna” di profil pengguna terkait.",
},
];
const forumTerms3: Term[] = [
{
text: "Meninjau setiap laporan dalam waktu 24 jam",
},
{
text: "Menghapus konten yang melanggar",
},
{
text: "Memblokir akun pelanggar sesuai tingkat pelanggaran",
},
];

View File

@@ -11,31 +11,52 @@ import Home_FeatureSection from "@/screens/Home/topFeatureSection";
import { apiUser } from "@/service/api-client/api-user";
import { apiVersion } from "@/service/api-config";
import { Ionicons } from "@expo/vector-icons";
import { Redirect, router, Stack } from "expo-router";
import { useEffect, useState } from "react";
import { Redirect, router, Stack, useFocusEffect } from "expo-router";
import { useCallback, useEffect, useState } from "react";
import { RefreshControl } from "react-native";
export default function Application() {
const { token, user } = useAuth();
const { token, user, userData } = useAuth();
const [data, setData] = useState<any>();
const [refreshing, setRefreshing] = useState(false);
console.log("[User] >>", JSON.stringify(user?.id, null, 2));
useEffect(() => {
onLoadData();
checkVersion();
}, []);
// ‼️ Untuk cek apakah: 1. user ada, 2. user punya profile, 3. accept temrs of forum nya ada atau tidak
useFocusEffect(
useCallback(() => {
onLoadData();
checkVersion();
userData(token as string);
}, [user?.id, token])
);
async function onLoadData() {
const response = await apiUser(user?.id as string);
console.log("Response profile >>", JSON.stringify(response?.data?.Profile, null, 2));
console.log(
"[Profile ID]>>",
JSON.stringify(response?.data?.Profile?.id, null, 2)
);
setData(response.data);
}
const checkVersion = async () => {
const response = await apiVersion();
console.log("Version >>", JSON.stringify(response.data, null, 2));
console.log("[Version] >>", JSON.stringify(response.data, null, 2));
};
const onRefresh = useCallback(() => {
setRefreshing(true);
onLoadData();
checkVersion();
setRefreshing(false);
}, []);
if (user && user?.termsOfServiceAccepted === false) {
console.log("User is not accept term service");
return <Redirect href={`/terms-agreement`} />;
}
if (data && data?.active === false) {
console.log("User is not active");
return <Redirect href={`/waiting-room`} />;
@@ -63,6 +84,7 @@ export default function Application() {
),
headerRight: () => (
<Ionicons
disabled={true}
name="notifications"
size={20}
color={MainColor.yellow}
@@ -74,8 +96,16 @@ export default function Application() {
}}
/>
<ViewWrapper
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
footerComponent={
<TabSection tabs={tabsHome(data?.Profile?.id as string)} />
<TabSection
tabs={tabsHome({
acceptedForumTermsAt: data?.acceptedForumTermsAt,
profileId: data?.Profile?.id,
})}
/>
}
>
<StackCustom>

View File

@@ -15,6 +15,7 @@ import Investment_ButtonInvestasiSection from "@/screens/Invesment/ButtonInvesta
import Invesment_ComponentBoxOnBottomDetail from "@/screens/Invesment/ComponentBoxOnBottomDetail";
import Invesment_DetailDataPublishSection from "@/screens/Invesment/DetailDataPublishSection";
import { apiInvestmentGetOne } from "@/service/api-client/api-investment";
import { countDownAndCondition } from "@/utils/countDownAndCondition";
import { AntDesign, MaterialIcons } from "@expo/vector-icons";
import {
router,
@@ -23,7 +24,7 @@ import {
useLocalSearchParams,
} from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { useCallback, useEffect, useState } from "react";
export default function InvestmentDetail() {
const { user } = useAuth();
@@ -62,6 +63,31 @@ export default function InvestmentDetail() {
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 = (
<Invesment_ComponentBoxOnBottomDetail
id={id as string}
@@ -71,7 +97,7 @@ export default function InvestmentDetail() {
);
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 (

View File

@@ -31,9 +31,9 @@ import {
import pickImage from "@/utils/pickImage";
import { Ionicons } from "@expo/vector-icons";
import { Image } from "expo-image";
import { useLocalSearchParams } from "expo-router";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useEffect, useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { Text, TouchableOpacity, View } from "react-native";
import PhoneInput, { ICountry } from "react-native-international-phone-number";
import { Avatar } from "react-native-paper";
@@ -76,7 +76,7 @@ export default function PortofolioCreate() {
function handleInputValue(phoneNumber: string) {
setInputValue(phoneNumber);
const callingCode = selectedCountry?.callingCode.replace(/^\+/, "") || "";
const fixNumber = inputValue.replace(/\s+/g, "");
let fixNumber = inputValue.replace(/\s+/g, "").replace(/^0+/, "");
const realNumber = callingCode + fixNumber;
setData({ ...data, tlpn: realNumber });
}
@@ -85,10 +85,12 @@ export default function PortofolioCreate() {
setSelectedCountry(country);
}
useEffect(() => {
onLoadMaster();
onLoadMasterSubBidangBisnis();
}, []);
useFocusEffect(
useCallback(() => {
onLoadMaster();
onLoadMasterSubBidangBisnis();
}, [])
);
const onLoadMaster = async () => {
try {

View File

@@ -244,7 +244,7 @@ export default function PortofolioEdit() {
const handleSubmitUpdate = async () => {
const callingCode = selectedCountry?.callingCode.replace(/^\+/, "") || "";
const fixNumber = data.tlpn.replace(/\s+/g, "");
let fixNumber = data.tlpn.replace(/\s+/g, "").replace(/^0+/, "");
const realNumber = callingCode + fixNumber;
const newData: IFormData = {

View File

@@ -0,0 +1,148 @@
import {
AvatarUsernameAndOtherComponent,
BadgeCustom,
ClickableCustom,
Divider,
SelectCustom,
TextCustom,
} from "@/components";
import ListEmptyComponent from "@/components/_ShareComponent/ListEmptyComponent";
import ListLoaderFooterComponent from "@/components/_ShareComponent/ListLoaderFooterComponent";
import ListSkeletonComponent from "@/components/_ShareComponent/ListSkeletonComponent";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import { usePaginatedApi } from "@/hooks/use-paginated-api";
import { apiGetBlocked } from "@/service/api-client/api-blocked";
import { apiMasterAppCategory } from "@/service/api-client/api-master";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useEffect, useRef, useState } from "react";
import { RefreshControl, View } from "react-native";
const PAGE_SIZE = 10;
export default function ProfileBlockedList() {
const { user } = useAuth();
const [masterApp, setMasterApp] = useState<any[]>([]);
const isInitialMount = useRef(true);
const {
data: listData,
loading,
refreshing,
hasMore,
search,
setSearch,
onRefresh,
loadMore,
} = usePaginatedApi({
fetcher: async (params: { page: number; search?: string }) => {
const response = await apiGetBlocked({
id: user?.id as any,
search: search,
page: String(params.page) as any,
});
return response.data;
},
initialSearch: "",
pageSize: PAGE_SIZE,
dependencies: [user?.id],
});
useEffect(() => {
fetchMasterApp();
}, []);
// 🔁 Refresh otomatis saat kembali ke halaman ini
useFocusEffect(
useCallback(() => {
if (isInitialMount.current) {
// Skip saat pertama kali mount
isInitialMount.current = false;
return;
}
// Hanya refresh saat kembali dari screen lain
onRefresh();
}, [onRefresh])
);
const fetchMasterApp = async () => {
const response = await apiMasterAppCategory();
setMasterApp(response.data);
};
const renderHeader = () => (
<SelectCustom
placeholder="Pilih Kategori Fitur"
data={masterApp.map((item) => ({
label: item.name,
value: item.id,
}))}
value={search === "" ? undefined : search}
onChange={(value) => {
setSearch(value as any);
}}
/>
);
const renderItem = ({ item }: { item: any }) => (
<>
<ClickableCustom
onPress={() => {
router.push(`/profile/${item.id}/detail-blocked`);
}}
>
<View
style={{
paddingInline: 8,
}}
>
<AvatarUsernameAndOtherComponent
avatarHref={`/profile/${item?.blocked?.Profile?.id}`}
avatar={item?.blocked?.Profile?.imageId}
name={item?.blocked?.username}
rightComponent={
<View style={{ flexDirection: "row", gap: 4 }}>
<BadgeCustom>
<TextCustom size={"small"} bold truncate>
{item?.menuFeature?.name}
</TextCustom>
</BadgeCustom>
</View>
}
/>
<Divider color="gray" />
</View>
</ClickableCustom>
</>
);
return (
<>
<NewWrapper
// headerComponent={renderHeader()}
listData={listData}
renderItem={renderItem}
onEndReached={loadMore}
refreshControl={
<RefreshControl
progressBackgroundColor={MainColor.yellow}
refreshing={refreshing}
onRefresh={onRefresh}
/>
}
ListFooterComponent={
hasMore && !refreshing ? <ListLoaderFooterComponent /> : null
}
ListEmptyComponent={
!loading && _.isEmpty(listData) ? (
<ListSkeletonComponent />
) : (
<ListEmptyComponent />
)
}
/>
</>
);
}

View File

@@ -0,0 +1,93 @@
import {
AlertDefaultSystem,
AvatarUsernameAndOtherComponent,
BaseBox,
BoxButtonOnFooter,
BoxWithHeaderSection,
ButtonCustom,
NewWrapper,
StackCustom,
TextCustom,
} from "@/components";
import AvatarAndBackground from "@/screens/Profile/AvatarAndBackground";
import {
apiGetBlockedById,
apiUnblock,
} from "@/service/api-client/api-blocked";
import { router, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useEffect, useState } from "react";
export default function ProfileDetailBlocked() {
const { id } = useLocalSearchParams();
const [data, setData] = useState<any>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
useEffect(() => {
fetchData();
}, [id]);
const fetchData = async () => {
const response = await apiGetBlockedById({ id: String(id) });
// console.log("[RESPONSE >>]", JSON.stringify(response, null, 2));
setData(response.data);
};
const handleSubmit = async () => {
try {
setIsLoading(true);
await apiUnblock({ id: String(id) });
router.back();
} catch (error) {
console.log("[ERROR >>]", JSON.stringify(error, null, 2));
} finally {
setIsLoading(false);
}
};
return (
<>
<NewWrapper
footerComponent={
<BoxButtonOnFooter>
<ButtonCustom
isLoading={isLoading}
onPress={() => {
AlertDefaultSystem({
title: "Buka Blokir",
message: "Apakah anda yakin ingin membuka blokir ini?",
textLeft: "Tidak",
textRight: "Ya",
onPressRight: () => {
handleSubmit();
},
});
}}
>
Buka Blokir
</ButtonCustom>
</BoxButtonOnFooter>
}
>
<BoxWithHeaderSection>
<StackCustom>
<AvatarUsernameAndOtherComponent
avatarHref={`/profile/${data?.blocked?.Profile?.id}`}
avatar={data?.blocked?.Profile?.imageId}
name={data?.blocked?.username}
/>
<TextCustom align="center">
Jika anda membuka blokir ini maka semua postingan terkait user ini
akan muncul kembali di beranda
<TextCustom bold color="red">
{" "}
{_.upperCase(data?.menuFeature?.name)}
</TextCustom>
</TextCustom>
</StackCustom>
</BoxWithHeaderSection>
</NewWrapper>
</>
);
}

View File

@@ -64,14 +64,18 @@ export default function Profile() {
};
const onLoadPortofolio = async (id: string) => {
const response = await apiGetPortofolio({ id: id });
const lastTwoByDate = response.data
.sort(
(a: any, b: any) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
) // urut desc
.slice(0, 2);
setListPortofolio(lastTwoByDate);
try {
const response = await apiGetPortofolio({ id: id });
const lastTwoByDate = response.data
.sort(
(a: any, b: any) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
) // urut desc
.slice(0, 2);
setListPortofolio(lastTwoByDate);
} catch (error) {
console.log("[ERROR]", error);
}
};
return (

View File

@@ -33,6 +33,16 @@ export default function ProfileLayout() {
name="create"
options={{ title: "Buat Profile", headerBackVisible: false }}
/>
<Stack.Screen
name="[id]/blocked-list"
options={{ title: "Blocked List", headerLeft: () => <BackButton /> }}
/>
<Stack.Screen
name="[id]/detail-blocked"
options={{ title: "Detail Blokir", headerLeft: () => <BackButton /> }}
/>
</Stack>
</>
);

View File

@@ -6,6 +6,7 @@ import {
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";
@@ -13,6 +14,7 @@ import _ from "lodash";
import { useCallback, useState } from "react";
export default function VotingBeranda() {
const { user } = useAuth();
const [listData, setListData] = useState<any>([]);
const [loadingGetData, setLoadingGetData] = useState(false);
const [search, setSearch] = useState("");
@@ -29,6 +31,7 @@ export default function VotingBeranda() {
const response = await apiVotingGetAll({
search,
category: "beranda",
userLoginId: user?.id,
});
if (response.success) {
setListData(response.data);

View File

@@ -134,7 +134,7 @@ export default function VotingDetailStatus() {
{data &&
data?.catatan &&
(status === "draft" || status === "rejected") && (
(status === "draft" || status === "reject") && (
<ReportBox text={data?.catatan} />
)}

View File

@@ -4,6 +4,7 @@ import {
ButtonCenteredOnly,
ButtonCustom,
InformationBox,
NewWrapper,
StackCustom,
ViewWrapper,
} from "@/components";
@@ -12,6 +13,7 @@ import { useAuth } from "@/hooks/use-auth";
import { apiUser } from "@/service/api-client/api-user";
import { Ionicons } from "@expo/vector-icons";
import { router } from "expo-router";
import { RefreshControl } from "react-native";
import Toast from "react-native-toast-message";
export default function WaitingRoom() {
@@ -33,7 +35,7 @@ export default function WaitingRoom() {
} else {
Toast.show({
type: "success",
text1: "Akun anda telah aktif", // text2: "Anda berhasil login",
text1: "Selamat ! Akun anda telah aktif", // text2: "Anda berhasil login",
});
router.replace(`/(application)/(user)/profile/create`);
}
@@ -82,10 +84,18 @@ export default function WaitingRoom() {
return (
<>
<ViewWrapper footerComponent={logoutButton()}>
<NewWrapper
footerComponent={logoutButton()}
refreshControl={
<RefreshControl refreshing={isLoading} onRefresh={handleCheck} />
}
>
<StackCustom>
<InformationBox text="Permohonan akses Anda sedang dalam proses verifikasi oleh admin. Harap tunggu, Anda akan menerima pemberitahuan melalui Whatsapp setelah disetujui." />
<ButtonCenteredOnly
<InformationBox
text="Akun Anda sedang menunggu aktivasi.
Silakan tunggu beberapa saat. Untuk memperbarui status, tarik layar ke bawah."
/>
{/* <ButtonCenteredOnly
isLoading={isLoading}
onPress={() => {
handleCheck();
@@ -93,9 +103,9 @@ export default function WaitingRoom() {
icon="refresh-ccw"
>
Check
</ButtonCenteredOnly>
</ButtonCenteredOnly> */}
</StackCustom>
</ViewWrapper>
</NewWrapper>
</>
);
}

View File

@@ -9,6 +9,7 @@ export default function ApplicationLayout() {
<Stack.Screen name="(user)" options={{ headerShown: false }} />
<Stack.Screen name="admin" options={{ headerShown: false }} />
{/* Take Picture */}
<Stack.Screen

View File

@@ -0,0 +1,129 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BoxButtonOnFooter,
ButtonCustom,
StackCustom,
TextCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import { MainColor } from "@/constants/color-palet";
import {
apiAdminMasterBusinessFieldById,
apiAdminMasterBusinessFieldUpdate,
} from "@/service/api-admin/api-master-admin";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import { Switch } from "react-native-paper";
import Toast from "react-native-toast-message";
export default function AdminAppInformation_BusinessFieldDetail() {
const { id } = useLocalSearchParams();
const [data, setData] = useState<any | null>(null);
const [isLoading, setIsLoading] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadDetail();
}, [id])
);
const onLoadDetail = async () => {
try {
const response = await apiAdminMasterBusinessFieldById({
id: id as string,
category: "bidang"
});
setData(response.data);
} catch (error) {
console.log("[ERROR]", error);
setData(null);
}
};
const handlerSubmit = async () => {
if (!data.name) {
Toast.show({
type: "error",
text1: "Lengkapi Data",
});
return;
}
try {
setIsLoading(true);
const response = await apiAdminMasterBusinessFieldUpdate({
id: id as string,
data: data,
category: "bidang",
});
if (!response.success) {
Toast.show({
type: "error",
text1: "Gagal update data",
});
return;
}
Toast.show({
type: "success",
text1: "Data berhasil di update",
});
router.back();
} catch (error) {
console.log(error);
} finally {
setIsLoading(false);
}
};
const buttonSubmit = (
<BoxButtonOnFooter>
<ButtonCustom
disabled={!data?.name}
isLoading={isLoading}
onPress={() => handlerSubmit()}
>
Update
</ButtonCustom>
</BoxButtonOnFooter>
);
return (
<>
<ViewWrapper footerComponent={buttonSubmit}>
<StackCustom>
<AdminBackButtonAntTitle title="Update Bidang Bisnis" />
<TextInputCustom
label="Nama Bidang Bisnis"
placeholder="Masukan Nama Bidang Bisnis"
required
value={data?.name}
onChangeText={(value) => setData({ ...data, name: value })}
/>
<StackCustom
gap={"sm"}
style={{
alignContent: "flex-start",
}}
>
<TextCustom>Status</TextCustom>
<Switch
style={{
alignSelf: "flex-start",
}}
color={MainColor.yellow}
value={data?.active}
onValueChange={(value) => setData({ ...data, active: value })}
/>
</StackCustom>
</StackCustom>
</ViewWrapper>
</>
);
}

View File

@@ -1,22 +1,22 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BoxButtonOnFooter,
ButtonCustom,
ActionIcon,
BaseBox,
CenterCustom,
LoaderCustom,
Spacing,
StackCustom,
TextCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import { IconEdit } from "@/components/_Icon";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
import { MainColor } from "@/constants/color-palet";
import {
apiAdminMasterBusinessFieldById,
apiAdminMasterBusinessFieldUpdate,
} from "@/service/api-admin/api-master-admin";
import { apiAdminMasterBusinessFieldById } from "@/service/api-admin/api-master-admin";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import { Switch } from "react-native-paper";
import Toast from "react-native-toast-message";
import { Divider } from "react-native-paper";
export default function AdminAppInformation_BusinessFieldDetail() {
const { id } = useLocalSearchParams();
@@ -33,8 +33,11 @@ export default function AdminAppInformation_BusinessFieldDetail() {
try {
const response = await apiAdminMasterBusinessFieldById({
id: id as string,
category: "all",
});
console.log("Response >>", JSON.stringify(response, null, 2));
setData(response.data);
} catch (error) {
console.log("[ERROR]", error);
@@ -42,73 +45,89 @@ export default function AdminAppInformation_BusinessFieldDetail() {
}
};
const handlerSubmit = async () => {
if (!data.name) {
Toast.show({
type: "error",
text1: "Lengkapi Data",
});
return;
}
try {
setIsLoading(true);
const response = await apiAdminMasterBusinessFieldUpdate({
id: id as string,
data: data,
});
if (!response.success) {
Toast.show({
type: "error",
text1: "Gagal update data",
});
return;
}
Toast.show({
type: "success",
text1: "Data berhasil di update",
});
router.back();
} catch (error) {
console.log(error);
} finally {
setIsLoading(false);
}
};
const buttonSubmit = (
<BoxButtonOnFooter>
<ButtonCustom
disabled={!data?.name}
isLoading={isLoading}
onPress={() => handlerSubmit()}
>
Update
</ButtonCustom>
</BoxButtonOnFooter>
);
return (
<>
<ViewWrapper footerComponent={buttonSubmit}>
<ViewWrapper>
<StackCustom>
<AdminBackButtonAntTitle title="Update Bidang Bisnis" />
<AdminBackButtonAntTitle title="Detail Bidang & Sub Bidang" />
<TextInputCustom
label="Nama Bidang Bisnis"
placeholder="Masukan Nama Bidang Bisnis"
required
value={data?.name}
onChangeText={(value) => setData({ ...data, name: value })}
/>
{!data ? (
<LoaderCustom />
) : (
<StackCustom gap={"xs"}>
<TextCustom bold>Nama Bidang</TextCustom>
<Spacing height={5} />
<BaseBox>
<StackCustom gap={"xs"}>
<TextCustom bold>
Status: {data?.bidang?.active ? "Aktif" : "Tidak Aktif"}
</TextCustom>
<GridSpan_NewComponent
span1={10}
span2={2}
text1={
<TextCustom bold size={"large"}>
{data?.bidang?.name}
</TextCustom>
}
text2={
<CenterCustom>
<ActionIcon
icon={<IconEdit size={16} color={MainColor.black} />}
onPress={() =>
router.push(
`/admin/app-information/business-field/${id}/bidang-update`
)
}
/>
</CenterCustom>
}
/>
</StackCustom>
</BaseBox>
{/* <Divider /> */}
<Spacing height={5} />
<TextCustom>Status Aktivasi</TextCustom>
<Switch
color={MainColor.yellow}
value={data?.active}
onValueChange={(value) => setData({ ...data, active: value })}
/>
<TextCustom bold>Sub Bidang Bisnis</TextCustom>
<Spacing height={5} />
{data?.subBidang?.map((item: any, index: number) => (
<BaseBox key={index}>
<StackCustom gap={0}>
<TextCustom bold>
Status: {item?.isActive ? "Aktif" : "Tidak Aktif"}
</TextCustom>
<GridSpan_NewComponent
span1={10}
span2={2}
text1={
<TextCustom bold size={"large"}>
{item.name}
</TextCustom>
}
text2={
<CenterCustom>
<ActionIcon
icon={
<IconEdit size={16} color={MainColor.black} />
}
onPress={() =>
router.push(
`/admin/app-information/business-field/${item?.id}/sub-bidang-update`
)
}
/>
</CenterCustom>
}
/>
</StackCustom>
</BaseBox>
))}
</StackCustom>
)}
{/* <TextCustom>{JSON.stringify(data, null, 2)}</TextCustom> */}
</StackCustom>
</ViewWrapper>
</>

View File

@@ -0,0 +1,135 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BoxButtonOnFooter,
ButtonCustom,
StackCustom,
TextCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import { MainColor } from "@/constants/color-palet";
import {
apiAdminMasterBusinessFieldById,
apiAdminMasterBusinessFieldUpdate,
} from "@/service/api-admin/api-master-admin";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import { Switch } from "react-native-paper";
import Toast from "react-native-toast-message";
export default function AdminAppInformation_BusinessFieldDetail() {
const { id } = useLocalSearchParams();
const [data, setData] = useState<any | null>(null);
const [isLoading, setIsLoading] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadDetail();
}, [id])
);
const onLoadDetail = async () => {
try {
const response = await apiAdminMasterBusinessFieldById({
id: id as string,
category: "sub-bidang",
subBidangId: id as string,
});
console.log("Response >>", JSON.stringify(response, null, 2));
setData(response.data);
} catch (error) {
console.log("[ERROR]", error);
setData(null);
}
};
const handlerSubmit = async () => {
if (!data.name) {
Toast.show({
type: "error",
text1: "Lengkapi Data",
});
return;
}
try {
setIsLoading(true);
const response = await apiAdminMasterBusinessFieldUpdate({
id: id as string,
data: data,
category: "sub-bidang",
});
if (!response.success) {
Toast.show({
type: "error",
text1: "Gagal update data",
});
return;
}
Toast.show({
type: "success",
text1: "Data berhasil di update",
});
router.back();
} catch (error) {
console.log(error);
} finally {
setIsLoading(false);
}
};
const buttonSubmit = (
<BoxButtonOnFooter>
<ButtonCustom
disabled={!data?.name}
isLoading={isLoading}
onPress={() => handlerSubmit()}
>
Update
</ButtonCustom>
</BoxButtonOnFooter>
);
return (
<>
<ViewWrapper footerComponent={buttonSubmit}>
<StackCustom>
<AdminBackButtonAntTitle title="Update Bidang Bisnis" />
<TextInputCustom
label="Nama Bidang Bisnis"
placeholder="Masukan Nama Bidang Bisnis"
required
value={data?.name}
onChangeText={(value) => setData({ ...data, name: value })}
/>
<StackCustom
gap={"sm"}
style={{
alignContent: "flex-start",
}}
>
<TextCustom>Status</TextCustom>
<Switch
style={{
alignSelf: "flex-start",
}}
color={MainColor.yellow}
value={data?.isActive}
onValueChange={(value) => setData({ ...data, isActive: value })}
/>
</StackCustom>
</StackCustom>
</ViewWrapper>
</>
);
}

View File

@@ -1,40 +1,81 @@
import {
BoxButtonOnFooter,
ButtonCustom,
StackCustom,
TextInputCustom,
ViewWrapper,
ActionIcon,
BoxButtonOnFooter,
ButtonCustom,
CenterCustom,
Grid,
Spacing,
StackCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
import { apiAdminMasterBusinessFieldCreate } from "@/service/api-admin/api-master-admin";
import { Ionicons } from "@expo/vector-icons";
import { router } from "expo-router";
import _ from "lodash";
import { useState } from "react";
import { View } from "react-native";
import { Divider } from "react-native-paper";
import Toast from "react-native-toast-message";
export default function AdminAppInformation_BusinessFieldCreate() {
const [data, setData] = useState<any>({
const [isLoading, setIsLoading] = useState(false);
const [bidang, setBidang] = useState<any>({
name: "",
});
const [isLoading, setIsLoading] = useState(false);
const [subBidang, setSubBidang] = useState<any[]>([
{
name: "",
},
]);
const handlerSubmit = async () => {
if (!data.name) {
if (!bidang.name) {
Toast.show({
type: "error",
text1: "Lengkapi Data",
});
return;
}
if (subBidang[0].name === "") {
Toast.show({
type: "error",
text1: "Lengkapi Sub Bidang",
});
return;
}
try {
setIsLoading(true);
const response = await apiAdminMasterBusinessFieldCreate({ data: data });
const newData = {
bidang: bidang,
subBidang: subBidang,
};
console.log("[DATA]", newData);
const response = await apiAdminMasterBusinessFieldCreate({
data: newData,
});
console.log("[RESPONSE]", response);
if (response.success) {
Toast.show({
type: "success",
text1: "Data berhasil di tambah",
});
router.back();
// router.back();
} else {
Toast.show({
type: "error",
text1: "Gagal tambah data",
});
}
} catch (error) {
console.log("[ERROR]", error);
@@ -50,6 +91,7 @@ export default function AdminAppInformation_BusinessFieldCreate() {
const buttonSubmit = (
<BoxButtonOnFooter>
<ButtonCustom
disabled={subBidang[0].name === ""}
onPress={() => handlerSubmit()}
isLoading={isLoading}
>
@@ -60,16 +102,70 @@ export default function AdminAppInformation_BusinessFieldCreate() {
return (
<>
<ViewWrapper footerComponent={buttonSubmit}>
<StackCustom>
<StackCustom gap={"xs"}>
<AdminBackButtonAntTitle title="Tambah Bidang Bisnis" />
<TextInputCustom
label="Nama Bidang Bisnis"
placeholder="Masukan Nama Bidang Bisnis"
required
value={data.name}
onChangeText={(value) => setData({ ...data, name: value })}
value={bidang.name}
onChangeText={(value) => setBidang({ ...bidang, name: value })}
/>
<Divider />
<Spacing height={5} />
{subBidang.map((item, index) => (
<TextInputCustom
key={index}
label="Nama Sub Bidang"
placeholder="Masukan Nama Sub Bidang"
required
value={item.name}
onChangeText={(value) => {
const list = _.clone(subBidang);
list[index].name = value;
setSubBidang(list);
}}
/>
))}
<CenterCustom>
<View
style={{ flexDirection: "row", alignItems: "center", gap: 10 }}
>
<ActionIcon
onPress={() => {
setSubBidang([...subBidang, { name: "" }]);
}}
icon={
<Ionicons
name="add-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
<ActionIcon
disabled={subBidang.length <= 1}
onPress={() => {
const list = _.clone(subBidang);
list.pop();
setSubBidang(list);
}}
icon={
<Ionicons
name="remove-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
</View>
</CenterCustom>
</StackCustom>
</ViewWrapper>
</>

View File

@@ -75,7 +75,7 @@ const listPage = [
},
{
id: "2",
label: "Bidang Bisnis",
label: "Bidang & Sub Bidang",
value: "business",
},
{

View File

@@ -2,11 +2,13 @@
import {
BaseBox,
Grid,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import { apiAdminCollaborationGetById } from "@/service/api-admin/api-admin-collaboration";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
@@ -28,6 +30,8 @@ export default function AdminCollaborationGroup() {
category: "group",
});
console.log("[DATA]", JSON.stringify(response.data, null, 2));
if (response.success) {
setData(response.data);
}
@@ -59,38 +63,33 @@ export default function AdminCollaborationGroup() {
))}
</StackCustom>
</BaseBox>
<TextCustom bold>Anggota</TextCustom>
<Spacing height={5}/>
<BaseBox>
<StackCustom>
<TextCustom align="center">Anggota</TextCustom>
<Grid>
<Grid.Col span={6} style={{ justifyContent: "center", paddingRight: 10 }}>
<TextCustom bold>Nomor</TextCustom>
</Grid.Col>
<Grid.Col span={6} style={{ justifyContent: "center" }}>
<TextCustom bold>Username</TextCustom>
</Grid.Col>
</Grid>
{data?.ProjectCollaboration_AnggotaRoomChat?.map(
(item: any, index: number) => (
<StackCustom key={index} gap={0}>
<Grid>
<Grid.Col
span={4}
style={{ justifyContent: "center", paddingRight: 10 }}
>
<TextCustom bold>Nama</TextCustom>
</Grid.Col>
<Grid.Col span={8} style={{ justifyContent: "center" }}>
<TextCustom>
{item?.User?.Profile?.name || "-"}
</TextCustom>
</Grid.Col>
</Grid>
<Grid>
<Grid.Col
span={4}
style={{ justifyContent: "center", paddingRight: 10 }}
>
<TextCustom bold>Username</TextCustom>
</Grid.Col>
<Grid.Col span={8} style={{ justifyContent: "center" }}>
<TextCustom>{item?.User?.username || "-"}</TextCustom>
</Grid.Col>
</Grid>
</StackCustom>
<Grid key={index}>
<Grid.Col span={6} style={{ justifyContent: "center", paddingRight: 10 }}>
<TextCustom bold truncate>+{item?.User?.nomor || "-"}</TextCustom>
</Grid.Col>
<Grid.Col span={6} style={{ justifyContent: "center" }}>
<TextCustom bold>
{item?.User?.username || "-"}
</TextCustom>
</Grid.Col>
</Grid>
)
)}
</StackCustom>

View File

@@ -1,17 +1,14 @@
import {
ActionIcon,
ClickableCustom,
LoaderCustom,
StackCustom,
TextCustom,
ViewWrapper
} from "@/components";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
import { apiAdminCollaboration } from "@/service/api-admin/api-admin-collaboration";
import { Octicons } from "@expo/vector-icons";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
@@ -34,7 +31,7 @@ export default function AdminCollaborationGroup() {
const response = await apiAdminCollaboration({
category: "group",
});
if (response.success) {
setList(response.data);
}
@@ -51,10 +48,19 @@ export default function AdminCollaborationGroup() {
<StackCustom>
<AdminComp_BoxTitle title="Group" />
<>
<AdminTitleTable
title1="Aksi"
title2="Jumlah peserta"
title3="Nama group"
<GridSpan_NewComponent
span1={6}
span2={6}
text1={
<TextCustom bold truncate align="center">
Jumlah Anggota
</TextCustom>
}
text2={
<TextCustom bold truncate>
Nama Group
</TextCustom>
}
/>
<Divider />
@@ -67,31 +73,27 @@ export default function AdminCollaborationGroup() {
) : (
list?.map((item: any, index: number) => (
<View key={index}>
<AdminTableValue
value1={
<ActionIcon
icon={
<Octicons
name="eye"
size={ICON_SIZE_BUTTON}
color="black"
/>
}
onPress={() => {
router.push(`/admin/collaboration/${item.id}/group`);
}}
/>
}
value2={
<TextCustom truncate={1}>
{item?.ProjectCollaboration_AnggotaRoomChat?.length ||
"-"}
</TextCustom>
}
value3={
<TextCustom truncate={2}>{item?.name || "-"}</TextCustom>
}
/>
<ClickableCustom
onPress={() => {
router.push(`/admin/collaboration/${item.id}/group`);
}}
>
<GridSpan_NewComponent
span1={6}
span2={6}
text1={
<TextCustom truncate={1} align="center">
{item?.ProjectCollaboration_AnggotaRoomChat?.length ||
"-"}
</TextCustom>
}
text2={
<TextCustom truncate={2}>
{item?.name || "-"}
</TextCustom>
}
/>
</ClickableCustom>
</View>
))
)}

View File

@@ -1,6 +1,8 @@
import {
ActionIcon,
ClickableCustom,
LoaderCustom,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
@@ -9,6 +11,7 @@ import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage"
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { apiAdminCollaboration } from "@/service/api-admin/api-admin-collaboration";
import { Octicons } from "@expo/vector-icons";
@@ -51,11 +54,7 @@ export default function AdminCollaborationPublish() {
<StackCustom>
<AdminComp_BoxTitle title="Publish" />
<AdminTitleTable
title1="Aksi"
title2="Username"
title3="Judul Proyek"
/>
<GridSpan_NewComponent text1={<TextCustom bold>Username</TextCustom>} text2={<TextCustom bold>Judul Proyek</TextCustom>} />
{/* <Spacing height={10} /> */}
<Divider />
@@ -68,32 +67,26 @@ export default function AdminCollaborationPublish() {
) : (
list?.map((item: any, index: number) => (
<View key={index}>
<AdminTableValue
value1={
<ActionIcon
icon={
<Octicons
name="eye"
size={ICON_SIZE_BUTTON}
color="black"
/>
}
onPress={() => {
router.push(`/admin/collaboration/${item?.id}/publish`);
}}
/>
}
value2={
<TextCustom align="center" truncate={1}>
{item?.Author?.username || "-"}{" "}
</TextCustom>
}
value3={
<TextCustom align="center" truncate={2}>
{item?.title || "-"}
</TextCustom>
}
/>
<ClickableCustom
onPress={() => {
router.push(`/admin/collaboration/${item?.id}/publish`);
}}
>
<GridSpan_NewComponent
text1={
<TextCustom truncate={1}>
{item?.Author?.username || "-"}{" "}
</TextCustom>
}
text2={
<TextCustom truncate={2}>
{item?.title || "-"}
</TextCustom>
}
/>
</ClickableCustom>
<Spacing height={8}/>
<Divider/>
</View>
))
)}

View File

@@ -18,7 +18,7 @@ import { IconDot, IconList } from "@/components/_Icon/IconComponent";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
import AdminButtonReview from "@/components/_ShareComponent/Admin/ButtonReview";
import { GridDetail_4_8 } from "@/components/_ShareComponent/GridDetail_4_8";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import ReportBox from "@/components/Box/ReportBox";
import { ICON_SIZE_BUTTON, TEXT_SIZE_LARGE } from "@/constants/constans-value";
import AdminDonation_BoxOfDonationStory from "@/screens/Admin/Donation/BoxOfDonationStory";
@@ -195,7 +195,7 @@ export default function AdminDonationDetail() {
<StackCustom gap={5}>
{listPencarianDana.map((item, i) => (
<GridDetail_4_8
<GridSpan_4_8
key={i}
label={<TextCustom bold>{item.label}</TextCustom>}
value={<TextCustom>{item.value}</TextCustom>}
@@ -236,7 +236,7 @@ export default function AdminDonationDetail() {
<Spacing />
<StackCustom gap={"xs"}>
<GridDetail_4_8
<GridSpan_4_8
label={<TextCustom bold>Jumlah Donatur</TextCustom>}
value={
<TextCustom>
@@ -244,7 +244,7 @@ export default function AdminDonationDetail() {
</TextCustom>
}
/>
<GridDetail_4_8
<GridSpan_4_8
label={<TextCustom bold>Dana Terkumpul</TextCustom>}
value={
<TextCustom>
@@ -261,7 +261,7 @@ export default function AdminDonationDetail() {
<StackCustom>
<DummyLandscapeImage imageId={data?.imageId || ""} />
{listData.map((item, i) => (
<GridDetail_4_8
<GridSpan_4_8
key={i}
label={<TextCustom bold>{item.label}</TextCustom>}
value={<TextCustom>{item.value}</TextCustom>}

View File

@@ -8,7 +8,7 @@ import {
ViewWrapper,
} from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import { GridDetail_4_8 } from "@/components/_ShareComponent/GridDetail_4_8";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import {
apiAdminDonationInvoiceDetailById,
apiAdminDonationInvoiceUpdateById,
@@ -177,7 +177,7 @@ export default function AdminDonasiTransactionDetail() {
<BaseBox>
<StackCustom>
{listData.map((item, index) => (
<GridDetail_4_8
<GridSpan_4_8
key={index}
label={<TextCustom bold>{item.label}</TextCustom>}
value={<TextCustom>{item.value}</TextCustom>}

View File

@@ -7,7 +7,7 @@ import {
ViewWrapper,
} from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import { GridDetail_4_8 } from "@/components/_ShareComponent/GridDetail_4_8";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import { apiAdminDonationDisbursementOfFundsListById } from "@/service/api-admin/api-admin-donation";
import { dateTimeView } from "@/utils/dateTimeView";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
@@ -67,7 +67,7 @@ export default function AdminDonationDetailDisbursementOfFunds() {
<BaseBox>
<StackCustom>
{listData?.map((item, index) => (
<GridDetail_4_8
<GridSpan_4_8
key={index}
label={<TextCustom bold>{item.label}</TextCustom>}
value={<TextCustom>{item.value}</TextCustom>}

View File

@@ -1,17 +1,56 @@
import {
BoxButtonOnFooter,
ButtonCustom,
StackCustom,
TextCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import { MainColor } from "@/constants/color-palet";
import { apiAdminMasterDonationCategoryCreate } from "@/service/api-admin/api-master-admin";
import { useRouter } from "expo-router";
import { useState } from "react";
import { Switch } from "react-native-paper";
import Toast from "react-native-toast-message";
export default function AdminDonationCategoryCreate() {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [data, setData] = useState({
name: "",
active: false,
});
const onSubmit = async () => {
try {
setLoading(true);
const response = await apiAdminMasterDonationCategoryCreate({ data });
if (response.success) {
Toast.show({
type: "success",
text2: "Data berhasil disimpan",
});
router.back();
return;
}
Toast.show({
type: "error",
text1: "Gagal menyimpan data",
});
} catch (error) {
console.log("[Error]", error);
} finally {
setLoading(false);
}
};
const buttonSubmit = (
<BoxButtonOnFooter>
<ButtonCustom onPress={() => router.back()}>Simpan</ButtonCustom>
<ButtonCustom isLoading={loading} onPress={onSubmit}>
Simpan
</ButtonCustom>
</BoxButtonOnFooter>
);
return (
@@ -20,7 +59,23 @@ export default function AdminDonationCategoryCreate() {
headerComponent={<AdminBackButtonAntTitle title="Tambah Kategori" />}
footerComponent={buttonSubmit}
>
<TextInputCustom placeholder="Masukkan Kategori" />
<TextInputCustom
label=""
placeholder="Masukkan Kategori"
value={data.name}
onChangeText={(text) => setData({ ...data, name: text })}
/>
<StackCustom gap={"sm"}>
<TextCustom>Status</TextCustom>
<Switch
style={{
alignSelf: "flex-start",
}}
color={MainColor.yellow}
value={data.active}
onValueChange={(value) => setData({ ...data, active: value })}
/>
</StackCustom>
</ViewWrapper>
</>
);

View File

@@ -1,21 +1,76 @@
import {
AlertDefaultSystem,
BoxButtonOnFooter,
ButtonCustom,
StackCustom,
TextCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import { MainColor } from "@/constants/color-palet";
import {
apiAdminMasterDonationCategoryById,
apiAdminMasterDonationCategoryUpdate,
} from "@/service/api-admin/api-master-admin";
import { useLocalSearchParams, useRouter } from "expo-router";
import { useState } from "react";
import { useCallback, useEffect, useState } from "react";
import { Switch } from "react-native-paper";
export default function AdminDonationCategoryUpdate() {
const router = useRouter();
const { id } = useLocalSearchParams();
const [value, setValue] = useState(id);
const router = useRouter();
const [data, setData] = useState<any>(null);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const fetchData = async () => {
const response = await apiAdminMasterDonationCategoryById({
id: id as any,
});
console.log(JSON.stringify(response.data, null, 2));
setData(response.data);
};
fetchData();
}, [id]);
const handlerSubmit = async () => {
try {
setIsLoading(true);
const response = await apiAdminMasterDonationCategoryUpdate({
id: id as any,
data: data,
});
console.log(JSON.stringify(response.data, null, 2));
router.back();
} catch (error) {
console.log(error);
} finally {
setIsLoading(false);
}
};
const buttonSubmit = (
<BoxButtonOnFooter>
<ButtonCustom onPress={() => router.back()}>Update</ButtonCustom>
<ButtonCustom
disabled={isLoading || data?.name === ""}
isLoading={isLoading}
onPress={() => {
AlertDefaultSystem({
title: "Update Data",
message: "Apakah anda yakin ingin mengupdate data ini?",
textLeft: "Batal",
textRight: "Ya",
onPressLeft: () => {},
onPressRight: () => handlerSubmit(),
});
}}
>
Update
</ButtonCustom>
</BoxButtonOnFooter>
);
return (
@@ -25,10 +80,28 @@ export default function AdminDonationCategoryUpdate() {
footerComponent={buttonSubmit}
>
<TextInputCustom
label="Nama Kategori"
placeholder="Masukkan Kategori"
value={value as any}
onChangeText={setValue}
value={data?.name}
onChangeText={(value) => setData({ ...data, name: value })}
/>
<StackCustom
gap={"sm"}
style={{
alignContent: "flex-start",
}}
>
<TextCustom>Status</TextCustom>
<Switch
style={{
alignSelf: "flex-start",
}}
color={MainColor.yellow}
value={data?.active}
onValueChange={(value) => setData({ ...data, active: value })}
/>
</StackCustom>
</ViewWrapper>
</>
);

View File

@@ -1,27 +1,65 @@
import {
ActionIcon,
BaseBox,
CenterCustom,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
BadgeCustom,
CenterCustom,
ClickableCustom,
Spacing,
StackCustom,
TextCustom,
ViewWrapper
} from "@/components";
import { IconEdit } from "@/components/_Icon";
import AdminActionIconPlus from "@/components/_ShareComponent/Admin/ActionIconPlus";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
import { GridView_3_3_6 } from "@/components/_ShareComponent/GridView_3_3_6";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { View } from "react-native";
import { Divider, Switch } from "react-native-paper";
import { router } from "expo-router";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import { apiAdminMasterDonationCategory } from "@/service/api-admin/api-master-admin";
import { colorActivationForBadge } from "@/utils/colorActivationForBadge";
import { router, useFocusEffect } from "expo-router";
import { useCallback, useState } from "react";
import { RefreshControl, View } from "react-native";
import { Divider } from "react-native-paper";
export default function AdminDonationCategory() {
const [listData, setListData] = useState<any[]>([]);
const [refreshing, setRefreshing] = useState(false);
const [loading, setLoading] = useState(false);
useFocusEffect(
useCallback(() => {
fetchMaster();
}, [])
);
const fetchMaster = async () => {
try {
setLoading(true);
const response = await apiAdminMasterDonationCategory();
if (response.success) {
console.log(JSON.stringify(response.data, null, 2));
setListData(response.data);
} else {
setListData([]);
}
} catch (error) {
console.log("[Error]", error);
} finally {
setLoading(false);
}
};
const onRefresh = async () => {
setRefreshing(true);
await fetchMaster();
setRefreshing(false);
};
return (
<>
<ViewWrapper headerComponent={<AdminTitlePage title="Donasi" />}>
<ViewWrapper
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={onRefresh} />
}
headerComponent={<AdminTitlePage title="Donasi" />}
>
<AdminComp_BoxTitle
title="Kategori"
rightComponent={
@@ -33,81 +71,65 @@ export default function AdminDonationCategory() {
}
/>
<BaseBox>
<GridView_3_3_6
component1={
<TextCustom bold align="center">
Aksi
</TextCustom>
}
component2={<TextCustom bold>Status</TextCustom>}
component3={<TextCustom bold>Kategori</TextCustom>}
<View>
<GridSpan_4_8
label={<TextCustom bold>Status</TextCustom>}
value={<TextCustom bold>Kategori</TextCustom>}
/>
{/* <Grid>
<Grid.Col style={{ paddingLeft: 10 }} span={4}>
<TextCustom bold>Status</TextCustom>
</Grid.Col>
<Grid.Col span={8}>
<TextCustom bold>Kategori</TextCustom>
</Grid.Col>
</Grid> */}
<Divider />
<Spacing />
<StackCustom>
{listData.map((item, index) => (
<View key={index}>
<GridView_3_3_6
component1={
<ClickableCustom
onPress={() => {
router.push(`/admin/donation/category-update?id=${item.id}`);
}}
key={index}
>
<GridSpan_4_8
label={
<CenterCustom>
<ActionIcon
icon={
<IconEdit size={ICON_SIZE_BUTTON} color="black" />
}
onPress={() => {
router.push(`/admin/donation/category-update?id=${index}`);
}}
/>
<BadgeCustom
color={colorActivationForBadge({
status: item.active,
})}
>
{item.active ? "Aktif" : "Tidak Aktif"}
</BadgeCustom>
</CenterCustom>
}
component2={
<Switch
value={true}
onValueChange={(item) => {
console.log(item);
}}
color={MainColor.yellow}
/>
}
component3={<TextCustom bold>{item.label}</TextCustom>}
value={<TextCustom>{item.name}</TextCustom>}
/>
<Spacing height={10} />
{/* <Grid containerStyle={{ paddingBottom: 10 }}>
<Grid.Col span={4} style={{ paddingLeft: 10 }}>
<CenterCustom>
<BadgeCustom
color={item.active ? MainColor.green : MainColor.red}
>
{item.active ? "Aktif" : "Tidak Aktif"}
</BadgeCustom>
</CenterCustom>
</Grid.Col>
<Grid.Col span={8}>
<TextCustom bold>{item.name}</TextCustom>
</Grid.Col>
</Grid> */}
<Divider />
</View>
</ClickableCustom>
))}
</StackCustom>
</BaseBox>
</View>
</ViewWrapper>
</>
);
}
const listData = [
{
label: "Kegiatan Sosial",
value: "kegiatan_sosial",
},
{
label: "Pendidikan",
value: "pendidikan",
},
{
label: "Kesehatan",
value: "kesehatan",
},
{
label: "Kebudayaan",
value: "kebudayaan",
},
{
label: "Bencana Alami",
value: "bencana_alami",
},
{
label: "Lainnya",
value: "lainnya",
},
];

View File

@@ -1,22 +1,22 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ActionIcon,
AlertDefaultSystem,
BadgeCustom,
BaseBox,
DrawerCustom,
LoaderCustom,
MenuDrawerDynamicGrid,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
ActionIcon,
AlertDefaultSystem,
BadgeCustom,
BaseBox,
DrawerCustom,
LoaderCustom,
MenuDrawerDynamicGrid,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { IconDot, IconList } from "@/components/_Icon/IconComponent";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
import AdminButtonReview from "@/components/_ShareComponent/Admin/ButtonReview";
import { GridDetail_4_8 } from "@/components/_ShareComponent/GridDetail_4_8";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import ReportBox from "@/components/Box/ReportBox";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";
@@ -39,6 +39,11 @@ export default function AdminEventDetail() {
const [data, setData] = React.useState<any | null>(null);
const [loadData, setLoadData] = React.useState(false);
const deepLinkURL = `${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;
useFocusEffect(
useCallback(() => {
onLoadData();
@@ -156,7 +161,7 @@ export default function AdminEventDetail() {
<BaseBox>
<StackCustom>
{listData.map((item, i) => (
<GridDetail_4_8
<GridSpan_4_8
key={i}
label={<TextCustom bold>{item.label}</TextCustom>}
value={<TextCustom>{item.value}</TextCustom>}
@@ -181,7 +186,7 @@ export default function AdminEventDetail() {
<LoaderCustom />
) : (
<QRCode
value={deepLinkURL}
value={isDevLink}
size={200}
// logo={require("@/assets/images/logo-hipmi.png")}
// logoSize={70}
@@ -191,7 +196,7 @@ export default function AdminEventDetail() {
/>
)}
<TextCustom align="center">{deepLinkURL}</TextCustom>
<TextCustom align="center">{isDevLink}</TextCustom>
</StackCustom>
</BaseBox>
)}

View File

@@ -10,14 +10,17 @@ import {
} from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import { apiAdminEventListOfParticipants } from "@/service/api-admin/api-admin-event";
import dayjs, { Dayjs } from "dayjs";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { View } from "moti";
import { useCallback, useState } from "react";
export default function AdminEventListOfParticipants() {
const { id } = useLocalSearchParams();
const [listData, setListData] = useState<any[] | null>(null);
const [loadData, setLoadData] = useState(false);
const [startDate, setStartDate] = useState<Dayjs | undefined>();
useFocusEffect(
useCallback(() => {
@@ -32,8 +35,11 @@ export default function AdminEventListOfParticipants() {
id: id as string,
});
console.log("[DATA]", JSON.stringify(response, null, 2));
if (response.success) {
setListData(response.data);
setStartDate(dayjs(response.data.Event.tanggal));
}
} catch (error) {
console.log("[ERROR]", error);
@@ -42,7 +48,6 @@ export default function AdminEventListOfParticipants() {
}
};
return (
<>
<ViewWrapper
@@ -60,17 +65,35 @@ export default function AdminEventListOfParticipants() {
<Grid>
<Grid.Col span={6}>
<StackCustom gap={"sm"}>
<TextCustom bold truncate>{item?.User?.username}</TextCustom>
<TextCustom bold truncate>
{item?.User?.username}
</TextCustom>
<TextCustom>+{item?.User?.nomor}</TextCustom>
</StackCustom>
</Grid.Col>
<Grid.Col span={6} style={{ justifyContent: "center" }}>
<BadgeCustom
style={{ alignSelf: "flex-end" }}
color={item?.isPresent ? "green" : "red"}
>
{item?.isPresent ? "Hadir" : "Tidak Hadir"}
</BadgeCustom>
{startDate &&
startDate.subtract(1, "hour").diff(dayjs()) < 0 ? (
<BadgeCustom
style={{ alignSelf: "flex-end" }}
color={item?.isPresent ? "green" : "red"}
>
{item?.isPresent ? "Hadir" : "Tidak Hadir"}
</BadgeCustom>
) : (
<View
style={{
justifyContent: "flex-end",
}}
>
<BadgeCustom
style={{ alignSelf: "flex-end" }}
color="gray"
>
-
</BadgeCustom>
</View>
)}
</Grid.Col>
</Grid>
</BaseBox>

View File

@@ -1,11 +1,12 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ActionIcon,
ClickableCustom,
LoaderCustom,
SearchInput,
StackCustom,
TextCustom,
ViewWrapper
ViewWrapper,
} from "@/components";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
@@ -13,6 +14,7 @@ import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { apiAdminEvent } from "@/service/api-admin/api-admin-event";
import { dateTimeView } from "@/utils/dateTimeView";
import { Octicons } from "@expo/vector-icons";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
@@ -74,8 +76,8 @@ export default function AdminEventStatus() {
<StackCustom gap={"sm"}>
<AdminTitleTable
title1="Aksi"
title2="Username"
title1="Username"
title2="Tanggal"
title3="Judul Event"
/>
<Divider />
@@ -83,36 +85,47 @@ export default function AdminEventStatus() {
{loadData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center" size="small" color="gray">Belum ada data</TextCustom>
<TextCustom align="center" size="small" color="gray">
Belum ada data
</TextCustom>
) : (
listData?.map((item, index) => (
<AdminTableValue
<ClickableCustom
key={index}
value1={
<ActionIcon
icon={
<Octicons
name="eye"
size={ICON_SIZE_BUTTON}
color="black"
/>
}
onPress={() => {
router.push(`/admin/event/${item.id}/${status}`);
}}
/>
}
value2={
<TextCustom truncate={1}>
{item?.Author?.username || "-"}
</TextCustom>
}
value3={
<TextCustom align="center" truncate={2}>
{item?.title || "-"}
</TextCustom>
}
/>
onPress={() => {
router.push(`/admin/event/${item.id}/${status}`);
}}
>
<AdminTableValue
key={index}
value1={
<TextCustom truncate={1}>
{item?.Author?.username || "-"}
</TextCustom>
// <ActionIcon
// icon={
// <Octicons
// name="eye"
// size={ICON_SIZE_BUTTON}
// color="black"
// />
// }
// onPress={() => {
// router.push(`/admin/event/${item.id}/${status}`);
// }}
// />
}
value2={
<TextCustom truncate={1}>
{dateTimeView({ date: item?.tanggal })}
</TextCustom>
}
value3={
<TextCustom truncate={2}>{item?.title || "-"}</TextCustom>
}
/>
<Divider/>
</ClickableCustom>
))
)}
</StackCustom>

View File

@@ -11,7 +11,7 @@ import {
} from "@/components";
import { IconDot } from "@/components/_Icon/IconComponent";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import { GridDetail_4_8 } from "@/components/_ShareComponent/GridDetail_4_8";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
import { apiAdminForumPostingById } from "@/service/api-admin/api-admin-forum";
@@ -103,7 +103,7 @@ export default function AdminForumDetailPosting() {
<BaseBox>
<StackCustom gap={"sm"}>
{listDataAction.map((item, i) => (
<GridDetail_4_8
<GridSpan_4_8
key={i}
label={<TextCustom bold>{item.label}</TextCustom>}
value={<TextCustom>{item.value}</TextCustom>}

View File

@@ -3,6 +3,7 @@ import {
ActionIcon,
AlertDefaultSystem,
BaseBox,
CenterCustom,
DrawerCustom,
LoaderCustom,
MenuDrawerDynamicGrid,
@@ -16,7 +17,8 @@ import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButt
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
import { GridDetail_4_8 } from "@/components/_ShareComponent/GridDetail_4_8";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import {
@@ -27,6 +29,7 @@ import {
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { View } from "react-native";
import { Divider } from "react-native-paper";
import Toast from "react-native-toast-message";
@@ -95,24 +98,24 @@ export default function AdminForumReportComment() {
>
<BaseBox>
<StackCustom gap={"sm"}>
<GridDetail_4_8
label={<TextCustom bold>Username</TextCustom>}
value={<TextCustom>{data?.Author?.username || "-"}</TextCustom>}
<GridSpan_NewComponent
text1={<TextCustom bold>Username</TextCustom>}
text2={<TextCustom>{data?.Author?.username || "-"}</TextCustom>}
/>
<GridDetail_4_8
label={<TextCustom bold>Komentar</TextCustom>}
value={<TextCustom>{data?.komentar || "-"}</TextCustom>}
<GridSpan_NewComponent
text1={<TextCustom bold>Komentar</TextCustom>}
text2={<TextCustom>{data?.komentar || "-"}</TextCustom>}
/>
</StackCustom>
</BaseBox>
<AdminComp_BoxTitle title="Daftar Report Komentar" />
<StackCustom>
<AdminTitleTable
title1="Aksi"
title2="Pelapor"
title3="Kategori Report"
<StackCustom gap={"sm"}>
<GridSpan_NewComponent
text1={<TextCustom bold align="center">Aksi</TextCustom>}
text2={<TextCustom bold>Pelapor</TextCustom>}
text3={<TextCustom bold>Kategori Report</TextCustom>}
/>
<Divider />
{loadList ? (
@@ -123,34 +126,39 @@ export default function AdminForumReportComment() {
</TextCustom>
) : (
listReport?.map((item: any, index: number) => (
<AdminTableValue
key={index}
value1={
<ActionIcon
icon={<IconView size={ICON_SIZE_BUTTON} color="black" />}
onPress={() => {
setOpenDrawerAction(true);
setSelectedReport({
id: item.id,
username: item.User?.username,
kategori: item.ForumMaster_KategoriReport?.title,
keterangan: item.ForumMaster_KategoriReport?.deskripsi,
deskripsi: item.deskripsi,
});
}}
/>
}
value2={
<TextCustom truncate={1}>
{item?.User?.username || "-"}
</TextCustom>
}
value3={
<TextCustom truncate={2} align="center">
{item?.ForumMaster_KategoriReport?.title || "-"}
</TextCustom>
}
/>
<View key={index}>
<GridSpan_NewComponent
text1={
<CenterCustom>
<ActionIcon
icon={<IconView size={ICON_SIZE_BUTTON} color="black" />}
onPress={() => {
setOpenDrawerAction(true);
setSelectedReport({
id: item.id,
username: item.User?.username,
kategori: item.ForumMaster_KategoriReport?.title,
keterangan:
item.ForumMaster_KategoriReport?.deskripsi,
deskripsi: item.deskripsi,
});
}}
/>
</CenterCustom>
}
text2={
<TextCustom truncate={1}>
{item?.User?.username || "-"}
</TextCustom>
}
text3={
<TextCustom truncate={2}>
{item?.ForumMaster_KategoriReport?.title || "-"}
</TextCustom>
}
/>
<Divider />
</View>
))
)}
</StackCustom>
@@ -208,20 +216,20 @@ export default function AdminForumReportComment() {
height={"auto"}
>
<StackCustom>
<GridDetail_4_8
<GridSpan_4_8
label={<TextCustom bold>Pelapor</TextCustom>}
value={<TextCustom>{selectedReport?.username || "-"}</TextCustom>}
/>
{selectedReport?.kategori && (
<>
<GridDetail_4_8
<GridSpan_4_8
label={<TextCustom bold>Kategori Report</TextCustom>}
value={
<TextCustom>{selectedReport?.kategori || "-"}</TextCustom>
}
/>
<GridDetail_4_8
<GridSpan_4_8
label={<TextCustom bold>Keterangan</TextCustom>}
value={
<TextCustom>{selectedReport?.keterangan || "-"}</TextCustom>
@@ -231,7 +239,7 @@ export default function AdminForumReportComment() {
)}
{selectedReport?.deskripsi && (
<GridDetail_4_8
<GridSpan_4_8
label={<TextCustom bold>Deskripsi</TextCustom>}
value={
<TextCustom>{selectedReport?.deskripsi || "-"}</TextCustom>

View File

@@ -4,6 +4,7 @@ import {
AlertDefaultSystem,
BadgeCustom,
BaseBox,
CenterCustom,
DrawerCustom,
LoaderCustom,
MenuDrawerDynamicGrid,
@@ -17,7 +18,8 @@ import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButt
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
import { GridDetail_4_8 } from "@/components/_ShareComponent/GridDetail_4_8";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import {
@@ -28,6 +30,7 @@ import {
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { View } from "react-native";
import { Divider } from "react-native-paper";
import Toast from "react-native-toast-message";
@@ -95,14 +98,14 @@ export default function AdminForumReportPosting() {
>
<BaseBox>
<StackCustom gap={"sm"}>
<GridDetail_4_8
label={<TextCustom bold>Username</TextCustom>}
value={<TextCustom>{data?.Author?.username || "-"}</TextCustom>}
<GridSpan_NewComponent
text1={<TextCustom bold>Username</TextCustom>}
text2={<TextCustom>{data?.Author?.username || "-"}</TextCustom>}
/>
<GridDetail_4_8
label={<TextCustom bold>Status</TextCustom>}
value={
<GridSpan_NewComponent
text1={<TextCustom bold>Status</TextCustom>}
text2={
data && data?.ForumMaster_StatusPosting?.status ? (
<BadgeCustom
color={
@@ -121,19 +124,23 @@ export default function AdminForumReportPosting() {
}
/>
<GridDetail_4_8
label={<TextCustom bold>Postingan</TextCustom>}
value={<TextCustom>{data?.diskusi || "-"}</TextCustom>}
<GridSpan_NewComponent
text1={<TextCustom bold>Postingan</TextCustom>}
text2={<TextCustom>{data?.diskusi || "-"}</TextCustom>}
/>
</StackCustom>
</BaseBox>
<AdminComp_BoxTitle title="Daftar Report Posting" />
<StackCustom gap={"sm"}>
<AdminTitleTable
title1="Aksi"
title2="Pelapor"
title3="Kategori Report"
<GridSpan_NewComponent
text1={
<TextCustom bold align="center">
Aksi
</TextCustom>
}
text2={<TextCustom bold>Pelapor</TextCustom>}
text3={<TextCustom bold>Kategori Report</TextCustom>}
/>
<Divider />
{loadListReport ? (
@@ -144,34 +151,41 @@ export default function AdminForumReportPosting() {
</TextCustom>
) : (
listReport?.map((item: any, index: number) => (
<AdminTableValue
key={index}
value1={
<ActionIcon
icon={<IconView size={ICON_SIZE_BUTTON} color="black" />}
onPress={() => {
setOpenDrawerAction(true);
setSelectedReport({
id: item?.id,
username: item?.User?.username,
kategori: item?.ForumMaster_KategoriReport?.title,
keterangan: item?.ForumMaster_KategoriReport?.deskripsi,
deskripsi: item?.deskripsi,
});
}}
/>
}
value2={
<TextCustom truncate={1}>
{item?.User?.username || "-"}
</TextCustom>
}
value3={
<TextCustom truncate={2} align="center">
{item?.ForumMaster_KategoriReport?.title || "-"}
</TextCustom>
}
/>
<View key={index}>
<GridSpan_NewComponent
text1={
<CenterCustom>
<ActionIcon
icon={
<IconView size={ICON_SIZE_BUTTON} color="black" />
}
onPress={() => {
setOpenDrawerAction(true);
setSelectedReport({
id: item?.id,
username: item?.User?.username,
kategori: item?.ForumMaster_KategoriReport?.title,
keterangan:
item?.ForumMaster_KategoriReport?.deskripsi,
deskripsi: item?.deskripsi,
});
}}
/>
</CenterCustom>
}
text2={
<TextCustom truncate>
{item?.User?.username || "-"}
</TextCustom>
}
text3={
<TextCustom truncate={2}>
{item?.ForumMaster_KategoriReport?.title || "-"}
</TextCustom>
}
/>
<Divider />
</View>
))
)}
</StackCustom>
@@ -229,20 +243,20 @@ export default function AdminForumReportPosting() {
height={"auto"}
>
<StackCustom>
<GridDetail_4_8
<GridSpan_4_8
label={<TextCustom bold>Pelapor</TextCustom>}
value={<TextCustom>{selectedReport?.username || "-"}</TextCustom>}
/>
{selectedReport?.kategori && (
<>
<GridDetail_4_8
<GridSpan_4_8
label={<TextCustom bold>Kategori Report</TextCustom>}
value={
<TextCustom>{selectedReport?.kategori || "-"}</TextCustom>
}
/>
<GridDetail_4_8
<GridSpan_4_8
label={<TextCustom bold>Keterangan</TextCustom>}
value={
<TextCustom>{selectedReport?.keterangan || "-"}</TextCustom>
@@ -252,7 +266,7 @@ export default function AdminForumReportPosting() {
)}
{selectedReport?.deskripsi && (
<GridDetail_4_8
<GridSpan_4_8
label={<TextCustom bold>Deskripsi</TextCustom>}
value={
<TextCustom>{selectedReport?.deskripsi || "-"}</TextCustom>

View File

@@ -1,22 +1,22 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ActionIcon,
ClickableCustom,
LoaderCustom,
SearchInput,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { IconView } from "@/components/_Icon/IconComponent";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
import { apiAdminForum } from "@/service/api-admin/api-admin-forum";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import React, { useCallback, useState } from "react";
import { useCallback, useState } from "react";
import { View } from "react-native";
import { Divider } from "react-native-paper";
export default function AdminForumPosting() {
@@ -37,7 +37,9 @@ export default function AdminForumPosting() {
category: "posting",
search: search,
});
console.log("DATA", JSON.stringify(response, null, 2));
if (response.success) {
setList(response.data);
}
@@ -51,7 +53,7 @@ export default function AdminForumPosting() {
const rightComponent = (
<SearchInput
containerStyle={{ width: "100%", marginBottom: 0 }}
placeholder="Cari"
placeholder="Cari postingan"
value={search}
onChangeText={setSearch}
/>
@@ -61,9 +63,15 @@ export default function AdminForumPosting() {
<>
<ViewWrapper headerComponent={<AdminTitlePage title="Forum" />}>
<AdminComp_BoxTitle title={"Posting"} rightComponent={rightComponent} />
<GridSpan_NewComponent
text1={<TextCustom bold truncate>Username</TextCustom>}
text2={<TextCustom bold truncate> Postingan</TextCustom>}
text3={<TextCustom bold align="center" truncate> Report Posting</TextCustom>}
text4={<TextCustom bold align="center" truncate> Komentar</TextCustom>}
/>
<Divider />
<Spacing />
<StackCustom>
<AdminTitleTable title1="Aksi" title2="Username" title3="Postingan" />
<Divider />
{loadList ? (
<LoaderCustom />
) : _.isEmpty(list) ? (
@@ -72,25 +80,38 @@ export default function AdminForumPosting() {
</TextCustom>
) : (
list?.map((item: any, index: number) => (
<AdminTableValue
key={index}
value1={
<ActionIcon
icon={<IconView size={ICON_SIZE_BUTTON} color="black" />}
onPress={() => {
router.push(`/admin/forum/${item?.id}`);
}}
<View key={index}>
<ClickableCustom
onPress={() => {
router.push(`/admin/forum/${item.id}`);
}}
>
<GridSpan_NewComponent
text1={
<TextCustom truncate={1}>
{item?.Author?.username || "-"}
</TextCustom>
}
text2={
<TextCustom truncate>
{item?.diskusi || "-"}
</TextCustom>
}
text3={
<TextCustom align="center" truncate={2}>
{item?.reportPosting || "-"}
</TextCustom>
}
text4={
<TextCustom align="center" truncate={2}>
{item?.komentar || "-"}
</TextCustom>
}
/>
}
value2={
<TextCustom truncate={1}>
{item?.Author?.username || "-"}
</TextCustom>
}
value3={
<TextCustom truncate={2}>{item?.diskusi || "-"}</TextCustom>
}
/>
</ClickableCustom>
<Divider />
</View>
))
)}
</StackCustom>

View File

@@ -1,8 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ActionIcon,
ClickableCustom,
LoaderCustom,
SearchInput,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
@@ -12,12 +14,14 @@ import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage"
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { apiAdminForum } from "@/service/api-admin/api-admin-forum";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { View } from "react-native";
import { Divider } from "react-native-paper";
export default function AdminForumReportComment() {
@@ -67,13 +71,26 @@ export default function AdminForumReportComment() {
rightComponent={rightComponent}
/>
<StackCustom gap={"sm"}>
<AdminTitleTable
title1="Aksi"
title2="Pelapor"
title3="Jenis Laporan"
/>
<Divider />
<GridSpan_NewComponent
text1={
<TextCustom bold truncate>
Pelapor
</TextCustom>
}
text2={
<TextCustom bold truncate>
Komentar
</TextCustom>
}
text3={
<TextCustom bold truncate>
Jenis Laporan
</TextCustom>
}
/>
<Divider />
<Spacing />
<StackCustom gap={"lg"}>
{loadList ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
@@ -82,34 +99,35 @@ export default function AdminForumReportComment() {
</TextCustom>
) : (
listData?.map((item: any, index: number) => (
<AdminTableValue
key={index}
value1={
<ActionIcon
icon={
<IconView
size={ICON_SIZE_BUTTON}
color={MainColor.black}
/>
<View key={index}>
<ClickableCustom
onPress={() => {
router.push(
`/admin/forum/${item?.Forum_Komentar?.id}/list-report-comment`
);
}}
>
<GridSpan_NewComponent
text1={
<TextCustom truncate={1}>
{item?.User?.username || "-"}
</TextCustom>
}
text2={
<TextCustom truncate={2}>
{item?.Forum_Komentar?.komentar || "-"}
</TextCustom>
}
text3={
<TextCustom truncate={2}>
{item?.ForumMaster_KategoriReport?.title || "-"}
</TextCustom>
}
onPress={() => {
router.push(
`/admin/forum/${item?.Forum_Komentar?.id}/list-report-comment`
);
}}
/>
}
value2={
<TextCustom truncate={1}>
{item?.User?.username || "-"}
</TextCustom>
}
value3={
<TextCustom truncate={2} align="center">
{item?.ForumMaster_KategoriReport?.title || "-"}
</TextCustom>
}
/>
</ClickableCustom>
<Spacing />
<Divider />
</View>
))
)}
</StackCustom>

View File

@@ -1,24 +1,27 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ActionIcon,
ClickableCustom,
Divider,
LoaderCustom,
SearchInput,
StackCustom,
TextCustom,
ViewWrapper
ViewWrapper,
} from "@/components";
import { IconView } from "@/components/_Icon/IconComponent";
import AdminComp_BoxTitle from "@/components/_ShareComponent/Admin/BoxTitlePage";
import AdminTitleTable from "@/components/_ShareComponent/Admin/TableTitle";
import AdminTableValue from "@/components/_ShareComponent/Admin/TableValue";
import AdminTitlePage from "@/components/_ShareComponent/Admin/TitlePage";
import { GridSpan_NewComponent } from "@/components/_ShareComponent/GridSpan_NewComponent";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import { apiAdminForum } from "@/service/api-admin/api-admin-forum";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { View } from "react-native";
export default function AdminForumReportPosting() {
const [listData, setListData] = useState<any[] | null>(null);
@@ -67,46 +70,51 @@ export default function AdminForumReportPosting() {
rightComponent={rightComponent}
/>
<StackCustom gap={"sm"}>
<AdminTitleTable title1="Aksi" title2="Pelapor" title3="Postingan" />
<Divider />
<GridSpan_NewComponent
text1={
<TextCustom bold truncate>
Username
</TextCustom>
}
text2={
<TextCustom bold truncate>
Postingan
</TextCustom>
}
/>
<Divider />
<StackCustom>
{loadList ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center" color="gray">
<TextCustom align="center" color="gray">
Belum ada data
</TextCustom>
) : (
listData?.map((item: any, index: number) => (
<AdminTableValue
key={index}
value1={
<ActionIcon
icon={
<IconView
size={ICON_SIZE_BUTTON}
color={MainColor.black}
/>
<View key={index}>
<ClickableCustom
onPress={() => {
router.push(
`/admin/forum/${item?.Forum_Posting?.id}/list-report-posting`
);
}}
>
<GridSpan_NewComponent
text1={
<TextCustom truncate={1}>
{item?.User?.username || "-"}
</TextCustom>
}
text2={
<TextCustom truncate={1}>
{item?.Forum_Posting?.diskusi || "-"}
</TextCustom>
}
onPress={() => {
router.push(
`/admin/forum/${item?.Forum_Posting?.id}/list-report-posting`
);
}}
/>
}
value2={
<TextCustom truncate={1}>
{item?.User?.username || "-"}
</TextCustom>
}
value3={
<TextCustom truncate={2} align="center">
{item?.Forum_Posting?.diskusi || "-"}
</TextCustom>
}
/>
</ClickableCustom>
<Divider />
</View>
))
)}
</StackCustom>

View File

@@ -19,7 +19,7 @@ import { IconDot, IconList } from "@/components/_Icon/IconComponent";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
import AdminButtonReview from "@/components/_ShareComponent/Admin/ButtonReview";
import { GridDetail_4_8 } from "@/components/_ShareComponent/GridDetail_4_8";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import ReportBox from "@/components/Box/ReportBox";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
@@ -183,7 +183,7 @@ export default function AdminInvestmentDetail() {
/>
<Spacing />
<StackCustom gap={"xs"}>
<GridDetail_4_8
<GridSpan_4_8
label={<TextCustom bold>Sisa Saham</TextCustom>}
value={
<TextCustom>
@@ -191,7 +191,7 @@ export default function AdminInvestmentDetail() {
</TextCustom>
}
/>
<GridDetail_4_8
<GridSpan_4_8
label={<TextCustom bold>Validasi Transaksi</TextCustom>}
value={
<TextCustom>
@@ -207,7 +207,7 @@ export default function AdminInvestmentDetail() {
<StackCustom>
<DummyLandscapeImage imageId={data?.imageId} />
{listData.map((item, i) => (
<GridDetail_4_8
<GridSpan_4_8
key={i}
label={<TextCustom bold>{item.label}</TextCustom>}
value={<TextCustom>{item.value}</TextCustom>}
@@ -218,7 +218,7 @@ export default function AdminInvestmentDetail() {
<BaseBox>
<StackCustom>
<GridDetail_4_8
<GridSpan_4_8
label={<TextCustom bold>File Prospektus</TextCustom>}
value={
<ButtonCustom
@@ -238,7 +238,7 @@ export default function AdminInvestmentDetail() {
</ButtonCustom>
}
/>
<GridDetail_4_8
<GridSpan_4_8
label={<TextCustom bold>File Dokumen</TextCustom>}
value={
<StackCustom>

View File

@@ -10,7 +10,7 @@ import {
ViewWrapper,
} from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import { GridDetail_4_8 } from "@/components/_ShareComponent/GridDetail_4_8";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import { MainColor } from "@/constants/color-palet";
import {
@@ -225,7 +225,7 @@ export default function AdminInvestmentTransactionDetail() {
<BaseBox>
<StackCustom>
{listData.map((item, index) => (
<GridDetail_4_8
<GridSpan_4_8
key={index}
label={<TextCustom bold>{item.label}</TextCustom>}
value={<TextCustom>{item.value}</TextCustom>}

View File

@@ -76,7 +76,7 @@ export default function SuperAdmin_ListUser() {
</TextCustom>
}
component2={
<TextCustom align="center" bold>
<TextCustom bold>
Username
</TextCustom>
}

View File

@@ -73,11 +73,7 @@ export default function AdminUserAccess() {
Aksi
</TextCustom>
}
component2={
<TextCustom align="center" bold>
Username
</TextCustom>
}
component2={<TextCustom bold>Username</TextCustom>}
component3={
<TextCustom align="center" bold>
Status Akses

View File

@@ -1,19 +1,19 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AlertDefaultSystem,
BadgeCustom,
BaseBox,
CircleContainer,
Grid,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
AlertDefaultSystem,
BadgeCustom,
BaseBox,
CircleContainer,
Grid,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import AdminBackButtonAntTitle from "@/components/_ShareComponent/Admin/BackButtonAntTitle";
import AdminButtonReject from "@/components/_ShareComponent/Admin/ButtonReject";
import AdminButtonReview from "@/components/_ShareComponent/Admin/ButtonReview";
import { GridDetail_4_8 } from "@/components/_ShareComponent/GridDetail_4_8";
import { GridSpan_4_8 } from "@/components/_ShareComponent/GridSpan_4_8";
import ReportBox from "@/components/Box/ReportBox";
import { MainColor } from "@/constants/color-palet";
import funUpdateStatusVoting from "@/screens/Admin/Voting/funUpdateStatus";
@@ -169,7 +169,7 @@ export default function AdminVotingDetail() {
<BaseBox>
<StackCustom>
{listData.map((item, i) => (
<GridDetail_4_8
<GridSpan_4_8
key={i}
label={<TextCustom bold>{item.label}</TextCustom>}
value={<TextCustom>{item.value}</TextCustom>}

View File

@@ -103,7 +103,7 @@ export default function AdminVotingStatus() {
</TextCustom>
}
value3={
<TextCustom align="center" truncate={2}>
<TextCustom truncate={2}>
{item?.title || "-"}
</TextCustom>
}

View File

@@ -91,7 +91,7 @@ export default function AdminVotingHistory() {
}
value2={<TextCustom truncate={1}>{item?.Author?.username || "-"}</TextCustom>}
value3={
<TextCustom align="center" truncate={2}>
<TextCustom truncate={2}>
{item?.title || "-"}
</TextCustom>
}

View File

@@ -0,0 +1,116 @@
import {
BoxButtonOnFooter,
ButtonCustom,
CheckboxCustom,
InformationBox,
StackCustom,
ViewWrapper
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import { apiAcceptTermService, BASE_URL } from "@/service/api-config";
import { GStyles } from "@/styles/global-styles";
import { openBrowser } from "@/utils/openBrower";
import { Stack } from "expo-router";
import { useState } from "react";
import { Text, View } from "react-native";
import Toast from "react-native-toast-message";
export default function TermsAgreement() {
const { user, logout } = useAuth();
const [term, setTerm] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const url = BASE_URL;
const handleSubmit = async () => {
try {
setIsLoading(true);
const response = await apiAcceptTermService({
data: {
id: user?.id as string,
termsOfServiceAccepted: term,
},
});
if (!response.success) {
Toast.show({
type: "error",
text1: "Gagal",
text2: response.message,
});
return;
}
Toast.show({
type: "success",
text1: "Anda berhasil menerima syarat & ketentuan",
text2: "Silahkan login kembali",
});
setTimeout(() => {
logout();
}, 2000);
} catch (error) {
console.log("error", error);
} finally {
setIsLoading(false);
}
};
const footerComponent = (
<BoxButtonOnFooter>
<ButtonCustom
onPress={handleSubmit}
disabled={!term}
isLoading={isLoading}
>
Setuju
</ButtonCustom>
</BoxButtonOnFooter>
);
return (
<>
<Stack.Screen
options={{
title: "Terms & Conditions",
}}
/>
<ViewWrapper footerComponent={footerComponent}>
<StackCustom>
<InformationBox text="Anda dialihkan ke halaman syarat & ketentuan, karena Anda belum menerima persetujuan syarat & ketentuan." />
<View
style={{
flexDirection: "row",
alignItems: "center",
marginTop: 16,
marginBottom: 16,
paddingInline: 10,
}}
>
<CheckboxCustom value={term} onChange={() => setTerm(!term)} />
<Text style={GStyles.textLabel}>
Saya setuju dengan{" "}
<Text
style={{
color: MainColor.yellow,
textDecorationLine: "underline",
}}
onPress={() => {
const toUrl = `${url}/terms-of-service.html`;
openBrowser(toUrl);
}}
>
Syarat & Ketentuan
</Text>{" "}
yang melarang konten tidak pantas dan perilaku merugikan.
</Text>
</View>
</StackCustom>
</ViewWrapper>
</>
);
}

View File

@@ -29,6 +29,7 @@
"expo-haptics": "~15.0.7",
"expo-image": "~3.0.8",
"expo-image-picker": "~17.0.8",
"expo-linear-gradient": "~15.0.7",
"expo-linking": "~8.0.8",
"expo-location": "~19.0.7",
"expo-notifications": "^0.32.13",
@@ -39,6 +40,7 @@
"expo-system-ui": "~6.0.7",
"expo-web-browser": "~15.0.9",
"lodash": "^4.17.21",
"moti": "^0.30.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.4",
@@ -340,6 +342,10 @@
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="],
"@emotion/is-prop-valid": ["@emotion/is-prop-valid@0.8.8", "", { "dependencies": { "@emotion/memoize": "0.7.4" } }, "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA=="],
"@emotion/memoize": ["@emotion/memoize@0.7.4", "", {}, "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw=="],
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.7.0", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw=="],
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
@@ -478,6 +484,18 @@
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="],
"@motionone/animation": ["@motionone/animation@10.18.0", "", { "dependencies": { "@motionone/easing": "^10.18.0", "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-9z2p5GFGCm0gBsZbi8rVMOAJCtw1WqBTIPw3ozk06gDvZInBPIsQcHgYogEJ4yuHJ+akuW8g1SEIOpTOvYs8hw=="],
"@motionone/dom": ["@motionone/dom@10.12.0", "", { "dependencies": { "@motionone/animation": "^10.12.0", "@motionone/generators": "^10.12.0", "@motionone/types": "^10.12.0", "@motionone/utils": "^10.12.0", "hey-listen": "^1.0.8", "tslib": "^2.3.1" } }, "sha512-UdPTtLMAktHiqV0atOczNYyDd/d8Cf5fFsd1tua03PqTwwCe/6lwhLSQ8a7TbnQ5SN0gm44N1slBfj+ORIhrqw=="],
"@motionone/easing": ["@motionone/easing@10.18.0", "", { "dependencies": { "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-VcjByo7XpdLS4o9T8t99JtgxkdMcNWD3yHU/n6CLEz3bkmKDRZyYQ/wmSf6daum8ZXqfUAgFeCZSpJZIMxaCzg=="],
"@motionone/generators": ["@motionone/generators@10.18.0", "", { "dependencies": { "@motionone/types": "^10.17.1", "@motionone/utils": "^10.18.0", "tslib": "^2.3.1" } }, "sha512-+qfkC2DtkDj4tHPu+AFKVfR/C30O1vYdvsGYaR13W/1cczPrrcjdvYCj0VLFuRMN+lP1xvpNZHCRNM4fBzn1jg=="],
"@motionone/types": ["@motionone/types@10.17.1", "", {}, "sha512-KaC4kgiODDz8hswCrS0btrVrzyU2CSQKO7Ps90ibBVSQmjkrt2teqta6/sOG59v7+dPnKMAg13jyqtMKV2yJ7A=="],
"@motionone/utils": ["@motionone/utils@10.18.0", "", { "dependencies": { "@motionone/types": "^10.17.1", "hey-listen": "^1.0.8", "tslib": "^2.3.1" } }, "sha512-3XVF7sgyTSI2KWvTf6uLlBJ5iAgRgmvp3bpuOiQJvInd4nZ19ET8lX5unn30SlmRH7hXbBbH+Gxd0m0klJ3Xtw=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" } }, "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA=="],
"@nicolo-ribaudo/chokidar-2": ["@nicolo-ribaudo/chokidar-2@2.1.8-no-fsevents.3", "", {}, "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ=="],
@@ -1226,6 +1244,8 @@
"expo-keep-awake": ["expo-keep-awake@15.0.7", "", { "peerDependencies": { "expo": "*", "react": "*" } }, "sha512-CgBNcWVPnrIVII5G54QDqoE125l+zmqR4HR8q+MQaCfHet+dYpS5vX5zii/RMayzGN4jPgA4XYIQ28ePKFjHoA=="],
"expo-linear-gradient": ["expo-linear-gradient@15.0.7", "", { "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-yF+y+9Shpr/OQFfy/wglB/0bykFMbwHBTuMRa5Of/r2P1wbkcacx8rg0JsUWkXH/rn2i2iWdubyqlxSJa3ggZA=="],
"expo-linking": ["expo-linking@8.0.8", "", { "dependencies": { "expo-constants": "~18.0.8", "invariant": "^2.2.4" }, "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-MyeMcbFDKhXh4sDD1EHwd0uxFQNAc6VCrwBkNvvvufUsTYFq3glTA9Y8a+x78CPpjNqwNAamu74yIaIz7IEJyg=="],
"expo-location": ["expo-location@19.0.7", "", { "peerDependencies": { "expo": "*" } }, "sha512-YNkh4r9E6ECbPkBCAMG5A5yHDgS0pw+Rzyd0l2ZQlCtjkhlODB55nMCKr5CZnUI0mXTkaSm8CwfoCO8n2MpYfg=="],
@@ -1304,6 +1324,10 @@
"form-data": ["form-data@4.0.4", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow=="],
"framer-motion": ["framer-motion@6.5.1", "", { "dependencies": { "@motionone/dom": "10.12.0", "framesync": "6.0.1", "hey-listen": "^1.0.8", "popmotion": "11.0.3", "style-value-types": "5.0.0", "tslib": "^2.1.0" }, "optionalDependencies": { "@emotion/is-prop-valid": "^0.8.2" }, "peerDependencies": { "react": ">=16.8 || ^17.0.0 || ^18.0.0", "react-dom": ">=16.8 || ^17.0.0 || ^18.0.0" } }, "sha512-o1BGqqposwi7cgDrtg0dNONhkmPsUFDaLcKXigzuTFC5x58mE8iyTazxSudFzmT6MEyJKfjjU8ItoMe3W+3fiw=="],
"framesync": ["framesync@6.0.1", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-fUY88kXvGiIItgNC7wcTOl0SNRCVXMKSWW2Yzfmn7EKNc+MpCzcz9DhdHcdjbrtN3c6R4H5dTY2jiCpPdysEjA=="],
"freeport-async": ["freeport-async@2.0.0", "", {}, "sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ=="],
"fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
@@ -1380,6 +1404,8 @@
"hermes-parser": ["hermes-parser@0.29.1", "", { "dependencies": { "hermes-estree": "0.29.1" } }, "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA=="],
"hey-listen": ["hey-listen@1.0.8", "", {}, "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q=="],
"hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
"hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="],
@@ -1734,6 +1760,8 @@
"mkdirp": ["mkdirp@3.0.1", "", { "bin": "dist/cjs/src/bin.js" }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
"moti": ["moti@0.30.0", "", { "dependencies": { "framer-motion": "^6.5.1" }, "peerDependencies": { "react-native-reanimated": "*" } }, "sha512-YN78mcefo8kvJaL+TZNyusq6YA2aMFvBPl8WiLPy4eb4wqgOFggJOjP9bUr2YO8PrAt0uusmRG8K4RPL4OhCsA=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
@@ -1850,6 +1878,8 @@
"pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="],
"popmotion": ["popmotion@11.0.3", "", { "dependencies": { "framesync": "6.0.1", "hey-listen": "^1.0.8", "style-value-types": "5.0.0", "tslib": "^2.1.0" } }, "sha512-Y55FLdj3UxkR7Vl3s7Qr4e9m0onSnP8W7d/xQLsoJM40vs6UKHFdygs6SWryasTZYqugMjm3BepCF4CWXDiHgA=="],
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
"postcss": ["postcss@8.4.49", "", { "dependencies": { "nanoid": "^3.3.7", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA=="],
@@ -2162,6 +2192,8 @@
"structured-headers": ["structured-headers@0.4.1", "", {}, "sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg=="],
"style-value-types": ["style-value-types@5.0.0", "", { "dependencies": { "hey-listen": "^1.0.8", "tslib": "^2.1.0" } }, "sha512-08yq36Ikn4kx4YU6RD7jWEv27v4V+PUsOGa4n/as8Et3CuODMJQ00ENeAVXAeydX4Z2j1XHZF1K2sX4mGl18fA=="],
"styleq": ["styleq@0.1.3", "", {}, "sha512-3ZUifmCDCQanjeej1f6kyl/BeP/Vae5EYkQ9iJfUm/QwZvlgnZzyflqAsAWYURdtea8Vkvswu2GrC57h3qffcA=="],
"sucrase": ["sucrase@3.35.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "glob": "^10.3.10", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA=="],

View File

@@ -0,0 +1,16 @@
import { Alert } from "react-native";
export default function AlertWarning({
title = "Peringatan Bagi Pengguna !",
description = "Konten yang Anda masukkan mengandung kata-kata yang tidak sesuai dengan pedoman komunitas kami. Mohon gunakan bahasa yang sopan dan menghargai sesama pengguna. Jika kata tersebut sebenarnya lumrah, mohon maaf—kemungkinan sistem kami belum mengenalnya sebagai wajar.",
}: {
title?: string
description?: string;
}) {
return Alert.alert(title, description, [
{
text: "Tutup",
onPress: () => {},
},
]);
}

View File

@@ -32,9 +32,10 @@ const FloatingButton: React.FC<FloatingButtonProps> = ({
const styles = StyleSheet.create({
fab: {
position: "absolute",
margin: 16,
margin: "auto",
right: 0,
bottom: 0,
// bottom: 10,
top: -20,
backgroundColor: AccentColor.softblue, // Warna Twitter biru
borderRadius: 50,
borderColor: AccentColor.blue,

View File

@@ -7,7 +7,7 @@ import DateTimePicker, {
} from "@react-native-community/datetimepicker";
import dayjs from "dayjs";
import React, { useState } from "react";
import { StyleProp, Text, View, ViewStyle } from "react-native";
import { Button, StyleProp, Text, View, ViewStyle } from "react-native";
import ClickableCustom from "../Clickable/ClickableCustom";
import TextCustom from "../Text/TextCustom";
@@ -129,24 +129,64 @@ const DateTimeInput_IOS: React.FC<DateTimeInputProps> = ({
borderWidth: 1,
}}
>
<View style={{ alignItems: "flex-end" }}>
{/* <View style={{ alignItems: "flex-start" }}>
<Ionicons
name="close"
size={20}
color="black"
onPress={() => setShow(false)}
onPress={() => {
setShow(false);
setSelectedDate(undefined);
}}
/>
</View>
</View> */}
<DateTimePicker
value={selectedDate || new Date()}
mode={"datetime"}
display="inline"
display="spinner"
onChange={handleConfirm}
minimumDate={minimumDate}
maximumDate={maximumDate}
themeVariant="light"
/>
<View style={{ flexDirection: "row", gap: 10 }}>
<ClickableCustom
onPress={() => {
setShow(false)
setSelectedDate(undefined)
}}
style={{
alignItems: "center",
justifyContent: "center",
padding: 12,
borderRadius: 10,
backgroundColor: MainColor.placeholder,
marginTop: 10,
width: "48%",
}}
>
<TextCustom color="black">Batal</TextCustom>
</ClickableCustom>
<ClickableCustom
onPress={() => {
setShow(false)
onChange(selectedDate as any)
}}
style={{
alignItems: "center",
justifyContent: "center",
padding: 12,
borderRadius: 10,
backgroundColor: MainColor.darkblue,
marginTop: 10,
width: "48%",
}}
>
<TextCustom>OK</TextCustom>
</ClickableCustom>
</View>
</View>
</>
)}

View File

@@ -87,7 +87,7 @@ const SelectCustom: React.FC<SelectProps> = ({
borderRadius,
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 10,
// paddingHorizontal: 0,
height: 50,
},

View File

@@ -1,3 +1,4 @@
import { AccentColor, MainColor } from "@/constants/color-palet";
import { GStyles } from "@/styles/global-styles";
import React, { useEffect, useState } from "react";
import {
@@ -6,7 +7,9 @@ import {
Text,
View,
ViewStyle,
useColorScheme,
} from "react-native";
import { PlaceholderColor } from "@/constants/color-palet";
type IconType = React.ReactNode | string;
@@ -48,7 +51,7 @@ const TextAreaCustom: React.FC<TextAreaCustomProps> = ({
minRows = 4,
maxRows = 6,
showCount = false,
maxLength,
maxLength = 1000,
value,
onChangeText,
height = 100,
@@ -78,6 +81,9 @@ const TextAreaCustom: React.FC<TextAreaCustomProps> = ({
);
};
const colorScheme = useColorScheme();
const theme = PlaceholderColor[colorScheme || "light"];
return (
<View style={[GStyles.inputContainerArea]}>
{label && (
@@ -109,6 +115,7 @@ const TextAreaCustom: React.FC<TextAreaCustomProps> = ({
GStyles.textAreaInput,
{ color: fontColor },
]}
placeholderTextColor={theme.placeholder}
editable={!disabled}
value={value as string}
onChangeText={onChangeText}

View File

@@ -1,3 +1,4 @@
import { PlaceholderColor } from "@/constants/color-palet";
import { GStyles } from "@/styles/global-styles";
import Ionicons from "@expo/vector-icons/Ionicons";
import React, { useState } from "react";
@@ -8,8 +9,10 @@ import {
TouchableOpacity,
View,
ViewStyle,
useColorScheme
} from "react-native";
type IconType = React.ReactNode | string;
type Props = {
@@ -74,6 +77,9 @@ const TextInputCustom = ({
}
};
const colorScheme = useColorScheme();
const theme = PlaceholderColor[colorScheme || "light"];
return (
<View style={[GStyles.inputContainerArea, containerStyle]}>
{label && (
@@ -100,12 +106,14 @@ const TextInputCustom = ({
{ color: fontColor },
disabled && GStyles.inputPlaceholderDisabled, // <-- placeholder saat disabled
]}
placeholderTextColor={theme.placeholder}
editable={!disabled}
secureTextEntry={secureTextEntry && !isPasswordVisible}
keyboardType={keyboardType}
onChangeText={handleTextChange}
maxLength={maxLength}
{...rest}
/>
{secureTextEntry && (
<TouchableOpacity

View File

@@ -15,7 +15,7 @@ export default function AdminButtonReject({
<>
<ButtonCustom
iconLeft={<IconReject size={16} />}
backgroundColor={MainColor.red}
backgroundColor={MainColor.orange}
textColor="white"
onPress={onReject}
isLoading={isLoading}

View File

@@ -1,17 +1,23 @@
import Grid from "@/components/Grid/GridCustom";
import React from "react";
import { View } from "react-native";
import { StyleProp, View, ViewStyle } from "react-native";
import { Divider } from "react-native-paper";
export default function AdminTableValue({
value1,
value2,
value3,
style1,
style2,
style3,
bottomLine = false,
}: {
value1: React.ReactNode;
value2: React.ReactNode;
value3: React.ReactNode;
style1?: ViewStyle;
style2?: ViewStyle;
style3?: ViewStyle;
bottomLine?: boolean;
}) {
return (
@@ -25,6 +31,7 @@ export default function AdminTableValue({
justifyContent: "center",
paddingLeft: 10,
paddingRight: 10,
...style1,
}}
>
{value1}
@@ -36,6 +43,7 @@ export default function AdminTableValue({
justifyContent: "center",
paddingLeft: 10,
paddingRight: 10,
...style2,
}}
>
{value2}
@@ -44,9 +52,10 @@ export default function AdminTableValue({
span={6}
style={{
justifyContent: "center",
alignItems: "center",
alignItems: "flex-start",
paddingLeft: 10,
paddingRight: 10,
...style3,
}}
>
{value3}

View File

@@ -1,20 +0,0 @@
import { Grid } from "@/components";
export const GridDetail_4_8 = ({
label,
value,
}: {
label: React.ReactNode;
value: React.ReactNode;
}) => {
return (
<Grid>
<Grid.Col span={4} style={{ justifyContent: "center", paddingRight: 10 }}>
{label}
</Grid.Col>
<Grid.Col span={8} style={{ justifyContent: "center" }}>
{value}
</Grid.Col>
</Grid>
);
};

View File

@@ -0,0 +1,27 @@
import { Grid } from "@/components";
export const GridSpan_4_8 = ({
label: text1,
value: text2,
}: {
label: React.ReactNode;
value: React.ReactNode;
}) => {
return (
<Grid>
<Grid.Col
span={4}
style={{
justifyContent: "flex-start",
paddingRight: 8,
paddingLeft: 8,
}}
>
{text1}
</Grid.Col>
<Grid.Col span={8} style={{ justifyContent: "center", paddingRight: 8 }}>
{text2}
</Grid.Col>
</Grid>
);
};

View File

@@ -0,0 +1,54 @@
import { Grid } from "@/components";
export const GridSpan_NewComponent = ({
text1,
text2,
text3,
text4,
span1,
span2,
}: {
text1: React.ReactNode;
text2: React.ReactNode;
text3?: React.ReactNode;
text4?: React.ReactNode;
span1?: number;
span2?: number;
}) => {
return (
<Grid>
<Grid.Col
span={span1 ? span1 : text4 ? 3 : 4}
style={{
justifyContent: "flex-start",
paddingRight: 5,
paddingLeft: 3,
}}
>
{text1}
</Grid.Col>
<Grid.Col
span={span2 ? span2 : text4 ? 3 : text3 ? 4 : 8}
style={{ justifyContent: "flex-start", paddingRight: 5 }}
>
{text2}
</Grid.Col>
{text3 && (
<Grid.Col
span={text4 ? 3 : 4}
style={{ justifyContent: "flex-start", paddingRight: 5 }}
>
{text3}
</Grid.Col>
)}
{text4 && (
<Grid.Col
span={3}
style={{ justifyContent: "flex-start", paddingRight: 5 }}
>
{text4}
</Grid.Col>
)}
</Grid>
);
};

View File

@@ -0,0 +1,20 @@
import { View } from "react-native";
import TextCustom from "../Text/TextCustom";
// Komponen Empty
const ListEmptyComponent = ({ search }: { search?: string }) => (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: 20,
}}
>
<TextCustom align="center" color="gray">
{search ? "Tidak ada hasil pencarian" : "Tidak ada data"}
</TextCustom>
</View>
);
export default ListEmptyComponent;

View File

@@ -0,0 +1,11 @@
import { View } from "react-native";
import LoaderCustom from "../Loader/LoaderCustom";
const ListLoaderFooterComponent = () =>(
<View style={{ paddingVertical: 16, alignItems: "center" }}>
<LoaderCustom />
</View>
)
export default ListLoaderFooterComponent;

View File

@@ -0,0 +1,21 @@
import { View } from "react-native";
import StackCustom from "../Stack/StackCustom";
import SkeletonCustom from "./SkeletonCustom";
const ListSkeletonComponent = ({
length = 5,
height = 100,
}: {
length?: number;
height?: number;
}) => (
<View style={{ flex: 1 }}>
<StackCustom>
{Array.from({ length }).map((_, i) => (
<SkeletonCustom height={height} key={i} />
))}
</StackCustom>
</View>
);
export default ListSkeletonComponent;

View File

@@ -0,0 +1,188 @@
// @/components/NewWrapper.tsx
import { MainColor } from "@/constants/color-palet";
import { OS_HEIGHT } from "@/constants/constans-value";
import { GStyles } from "@/styles/global-styles";
import {
ImageBackground,
Keyboard,
KeyboardAvoidingView,
Platform,
ScrollView,
FlatList,
TouchableWithoutFeedback,
View,
StyleProp,
ViewStyle,
} from "react-native";
import {
NativeSafeAreaViewProps,
SafeAreaView,
} from "react-native-safe-area-context";
import type { ScrollViewProps, FlatListProps } from "react-native";
// --- ✅ Tambahkan refreshControl ke BaseProps ---
interface BaseProps {
withBackground?: boolean;
headerComponent?: React.ReactNode;
footerComponent?: React.ReactNode;
floatingButton?: React.ReactNode;
hideFooter?: boolean;
edgesFooter?: NativeSafeAreaViewProps["edges"];
style?: StyleProp<ViewStyle>;
refreshControl?: ScrollViewProps["refreshControl"]; // ✅ dipakai di kedua mode
}
interface StaticModeProps extends BaseProps {
children: React.ReactNode;
listData?: never;
renderItem?: never;
}
interface ListModeProps extends BaseProps {
children?: never;
listData?: any[];
renderItem?: FlatListProps<any>["renderItem"];
onEndReached?: () => void;
// ✅ Gunakan tipe yang kompatibel dengan FlatList
ListHeaderComponent?: React.ReactElement | null;
ListFooterComponent?: React.ReactElement | null;
ListEmptyComponent?: React.ReactElement | null;
keyExtractor?: FlatListProps<any>["keyExtractor"];
}
type NewWrapperProps = StaticModeProps | ListModeProps;
const NewWrapper = (props: NewWrapperProps) => {
const {
withBackground = false,
headerComponent,
footerComponent,
floatingButton,
hideFooter = false,
edgesFooter = [],
style,
refreshControl, // ✅ sekarang ada di BaseProps
} = props;
const assetBackground = require("../../assets/images/main-background.png");
const renderContainer = (content: React.ReactNode) => {
if (withBackground) {
return (
<ImageBackground
source={assetBackground}
resizeMode="cover"
style={GStyles.imageBackground}
>
<View style={[GStyles.containerWithBackground, style]}>
{content}
</View>
</ImageBackground>
);
}
return <View style={[GStyles.container, style]}>{content}</View>;
};
// 🔹 Mode Dinamis
if ("listData" in props) {
const listProps = props as ListModeProps;
return (
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={{ flex: 1, backgroundColor: MainColor.darkblue }}
>
{headerComponent && (
<View style={GStyles.stickyHeader}>{headerComponent}</View>
)}
<View style={[GStyles.container, style]}>
<FlatList
data={listProps.listData}
renderItem={listProps.renderItem}
keyExtractor={
listProps.keyExtractor ||
((item) => {
if (item.id == null) {
console.warn("Item tanpa 'id':", item);
return `fallback-${JSON.stringify(item)}`;
}
return String(item.id);
})
}
refreshControl={refreshControl} // ✅ dari BaseProps
onEndReached={listProps.onEndReached}
onEndReachedThreshold={0.5}
ListHeaderComponent={listProps.ListHeaderComponent}
ListFooterComponent={listProps.ListFooterComponent}
ListEmptyComponent={listProps.ListEmptyComponent}
contentContainerStyle={{ flexGrow: 1 }}
keyboardShouldPersistTaps="handled"
/>
</View>
{footerComponent ? (
<SafeAreaView
edges={Platform.OS === "ios" ? edgesFooter : ["bottom"]}
style={{ backgroundColor: MainColor.darkblue, height: OS_HEIGHT }}
>
{footerComponent}
</SafeAreaView>
) : hideFooter ? null : (
<SafeAreaView
edges={["bottom"]}
style={{ backgroundColor: MainColor.darkblue }}
/>
)}
{floatingButton && (
<View style={GStyles.floatingContainer}>{floatingButton}</View>
)}
</KeyboardAvoidingView>
);
}
// 🔹 Mode Statis
const staticProps = props as StaticModeProps;
return (
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={{ flex: 1, backgroundColor: MainColor.darkblue }}
>
{headerComponent && (
<View style={GStyles.stickyHeader}>{headerComponent}</View>
)}
<ScrollView
contentContainerStyle={{ flexGrow: 1 }}
keyboardShouldPersistTaps="handled"
refreshControl={refreshControl} // ✅ sekarang valid
>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
{renderContainer(staticProps.children)}
</TouchableWithoutFeedback>
</ScrollView>
{footerComponent ? (
<SafeAreaView
edges={Platform.OS === "ios" ? edgesFooter : ["bottom"]}
style={{ backgroundColor: MainColor.darkblue, height: OS_HEIGHT }}
>
{footerComponent}
</SafeAreaView>
) : hideFooter ? null : (
<SafeAreaView
edges={["bottom"]}
style={{ backgroundColor: MainColor.darkblue }}
/>
)}
{floatingButton && (
<View style={GStyles.floatingContainer}>{floatingButton}</View>
)}
</KeyboardAvoidingView>
);
};
export default NewWrapper;

View File

@@ -0,0 +1,59 @@
// components/CustomSkeleton.tsx
import React from "react";
import { View, StyleProp, ViewStyle, DimensionValue } from "react-native";
import { MotiView } from "moti";
import { AccentColor, MainColor } from "@/constants/color-palet";
interface CustomSkeletonProps {
isLoading?: boolean;
style?: StyleProp<ViewStyle>;
width?: DimensionValue;
height?: DimensionValue;
radius?: number;
}
const CustomSkeleton: React.FC<CustomSkeletonProps> = ({
isLoading = true,
style,
width = "100%",
height = 16,
radius = 8,
}) => {
if (!isLoading) return null;
return (
<View
style={[
{
width,
height,
borderRadius: radius,
backgroundColor: AccentColor.darkblue,
overflow: "hidden",
position: "relative",
},
style,
]}
>
<MotiView
from={{ translateY: -100 }}
animate={{ translateY: 100 }}
transition={{
duration: 1200,
repeat: Infinity,
type: "timing",
}}
style={{
position: "absolute",
left: 0,
right: 0,
height: 100,
backgroundColor: MainColor.soft_darkblue,
borderRadius: 4,
}}
/>
</View>
);
};
export default CustomSkeleton;

View File

@@ -11,6 +11,7 @@ import {
View,
StyleProp,
ViewStyle,
ScrollViewProps,
} from "react-native";
import { NativeSafeAreaViewProps, SafeAreaView } from "react-native-safe-area-context";
@@ -23,6 +24,7 @@ interface ViewWrapperProps {
hideFooter?: boolean;
edgesFooter?: NativeSafeAreaViewProps["edges"];
style?: StyleProp<ViewStyle>;
refreshControl?: ScrollViewProps["refreshControl"];
}
/**
@@ -40,6 +42,7 @@ const ViewWrapper = ({
hideFooter = false,
edgesFooter =[],
style,
refreshControl,
}: ViewWrapperProps) => {
const assetBackground = require("../../assets/images/main-background.png");
@@ -57,6 +60,7 @@ const ViewWrapper = ({
<ScrollView
contentContainerStyle={{ flexGrow: 1 }}
keyboardShouldPersistTaps="handled"
refreshControl={refreshControl}
>
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={{ flex: 1 }}>

View File

@@ -59,6 +59,7 @@ import ViewWrapper from "./_ShareComponent/ViewWrapper";
import SearchInput from "./_ShareComponent/SearchInput";
import DummyLandscapeImage from "./_ShareComponent/DummyLandscapeImage";
import GridComponentView from "./_ShareComponent/GridSectionView";
import NewWrapper from "./_ShareComponent/NewWrapper";
// Progress
import ProgressCustom from "./Progress/ProgressCustom";
// Loader
@@ -119,6 +120,7 @@ export {
DummyLandscapeImage,
GridComponentView,
Spacing,
NewWrapper,
// Stack
StackCustom,
TabBarBackground,

View File

@@ -45,3 +45,23 @@ export const AdminColor = {
// Warna Asli: #002e59
// Warna Lebih Gelap: #001f3b
// Warna Tergelap: #001323
export const PlaceholderColor = {
light: {
text: "#000",
placeholder: "#666",
border: "#ccc",
background: "#fff",
error: "#d00",
icon: "#555",
},
dark: {
text: "#fff",
placeholder: "#aaa",
border: "#444",
background: "#1a1a1a",
error: "#ff4d4d",
icon: "#ccc",
},
};

View File

@@ -24,6 +24,7 @@ type AuthContextType = {
registerUser: (userData: {
username: string;
nomor: string;
termsOfServiceAccepted: boolean;
}) => Promise<void>;
userData: (token: string) => Promise<any>;
};
@@ -71,8 +72,31 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const loginWithNomor = async (nomor: string) => {
setIsLoading(true);
try {
console.log("[Masuk provider]", nomor);
const response = await apiLogin({ nomor: nomor });
await AsyncStorage.setItem("kode_otp", response.kodeId);
console.log("[RESPONSE AUTH]", JSON.stringify(response));
if (response.success) {
console.log("[Keluar provider]", nomor);
Toast.show({
type: "success",
text1: "Sukses",
text2: "Kode OTP berhasil dikirim",
});
await AsyncStorage.setItem("kode_otp", response.kodeId);
router.push(`/verification?nomor=${nomor}`);
return;
} else {
router.push(`/register?nomor=${nomor}`);
Toast.show({
type: "info",
text1: "Info",
text2: "Silahkan mendaftar",
});
return;
}
} catch (error: any) {
throw new Error(error.response?.data?.message || "Gagal kirim OTP");
} finally {
@@ -80,13 +104,26 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
}
};
// const loginWithNomor = async (nomor: string) => {
// setIsLoading(true);
// try {
// const response = await apiLogin({ nomor: nomor });
// await AsyncStorage.setItem("kode_otp", response.kodeId);
// } catch (error: any) {
// throw new Error(error.response?.data?.message || "Gagal kirim OTP");
// } finally {
// setIsLoading(false);
// }
// };
// --- 2. Validasi OTP & cek user ---
const validateOtp = async (nomor: string) => {
try {
setIsLoading(true);
const response = await apiValidationCode({ nomor: nomor });
const { token } = response;
console.log("[RESPONSE VALIDASI OTP]", JSON.stringify(response, null, 2));
if (response.success) {
setToken(token);
await AsyncStorage.setItem("authToken", token);
@@ -103,20 +140,23 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
if (response.active) {
if (response.roleId === "1") {
return "/(application)/(user)/home";
router.replace("/(application)/(user)/home");
return;
} else {
return "/(application)/admin/dashboard";
router.replace("/(application)/admin/dashboard");
return;
}
} else {
return "/(application)/(user)/waiting-room";
router.replace("/(application)/(user)/waiting-room");
return;
}
} else {
Toast.show({
type: "info",
text1: "Anda belum terdaftar",
text2: "Silahkan daftar terlebih dahulu",
text1: "Terjadi kesalahan",
text2: "Silahkan coba lagi",
});
return `/register?nomor=${nomor}`;
return;
}
} catch (error: any) {
console.log("Error validasi otp >>", (error as Error).message || error);
@@ -131,6 +171,10 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
// --- 3. Ambil data user ---
const userData = async (token: string) => {
try {
if (!token) {
throw new Error("Token tidak ditemukan");
}
setIsLoading(true);
const response = await apiConfig.get(`/mobile?token=${token}`, {
headers: {
@@ -144,7 +188,10 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
await AsyncStorage.setItem("userData", JSON.stringify(dataUser));
return dataUser;
} catch (error: any) {
console.log(error.response?.data?.message + "user" || "Gagal mengambil data user");
console.log(
"[LOAD USER DATA]",
error.response?.data?.message + "user" || "Gagal mengambil data user"
);
} finally {
setIsLoading(false);
}
@@ -154,12 +201,13 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
const registerUser = async (userData: {
username: string;
nomor: string;
termsOfServiceAccepted: boolean;
}) => {
setIsLoading(true);
try {
const response = await apiRegister({ data: userData });
console.log("[REGISTER FETCH]", JSON.stringify(response, null, 2));
const { token } = response;
if (!response.success) {
Toast.show({
type: "info",
@@ -170,23 +218,63 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
return;
}
setToken(token);
await AsyncStorage.setItem("authToken", token);
Toast.show({
type: "success",
text1: "Sukses",
text2: "Anda berhasil terdaftar",
});
router.replace("/(application)/(user)/waiting-room");
router.replace(`/verification?nomor=${userData.nomor}`);
return;
} catch (error: any) {
Toast.show({
type: "error",
text1: "Error",
text2: error.response?.data?.message || "Gagal mendaftar",
});
console.log("Error register", error);
} finally {
setIsLoading(false);
}
};
// const registerUser = async (userData: {
// username: string;
// nomor: string;
// termsOfServiceAccepted: boolean;
// }) => {
// setIsLoading(true);
// try {
// const response = await apiRegister({ data: userData });
// console.log("response", response);
// const { token } = response;
// if (!response.success) {
// Toast.show({
// type: "info",
// text1: "Info",
// text2: response.message,
// });
// return;
// }
// setToken(token);
// await AsyncStorage.setItem("authToken", token);
// Toast.show({
// type: "success",
// text1: "Sukses",
// text2: "Anda berhasil terdaftar",
// });
// router.replace("/(application)/(user)/waiting-room");
// return;
// } catch (error: any) {
// console.log("Error register", error);
// } finally {
// setIsLoading(false);
// }
// };
// --- 5. Logout ---
const logout = async () => {
try {
setIsLoading(true);

View File

@@ -0,0 +1,97 @@
// @/hooks/use-paginated-api.ts
import { useCallback, useState, useEffect, useRef } from "react";
interface UsePaginatedApiProps {
fetcher: (params: { page: number; search?: string }) => Promise<any[]>;
initialSearch?: string; // mengatur nilai awal dari state
pageSize?: number;
dependencies?: any[]; // untuk refresh saat deps berubah (misal: user.id)
}
export const usePaginatedApi = ({
fetcher,
initialSearch = "",
pageSize = 5,
dependencies = [],
}: UsePaginatedApiProps) => {
const [data, setData] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [page, setPage] = useState(1);
const [search, setSearch] = useState(initialSearch);
const refreshingRef = useRef<boolean>(false);
const loadingRef = useRef<boolean>(false);
const fetchRef = useRef(false);
const fetchData = useCallback(
async (pageNumber: number, clear: boolean) => {
const isRefresh = clear;
// 🔒 Proteksi: jangan jalankan jika sedang refreshing (untuk refresh)
if (isRefresh && refreshingRef.current) return;
// 🔒 Proteksi: jangan jalankan loadMore jika sedang loading
if (!isRefresh && loadingRef.current) return;
const setLoadingState = (isLoading: boolean) => {
if (isRefresh) {
setRefreshing(isLoading);
refreshingRef.current = isLoading;
} else {
setLoading(isLoading);
loadingRef.current = isLoading;
}
};
setLoadingState(true);
try {
const newData = await fetcher({ page: pageNumber, search });
setData((prev) => {
const current = Array.isArray(prev) ? prev : [];
return clear ? newData : [...current, ...newData];
});
setHasMore(newData.length === pageSize);
setPage(pageNumber);
} catch (error) {
console.error("[usePaginatedApi] Error:", error);
setHasMore(false);
} finally {
setLoadingState(false);
}
},
[search, hasMore, pageSize, ...dependencies]
);
const onRefresh = useCallback(() => {
fetchData(1, true);
}, [fetchData]);
const loadMore = useCallback(() => {
if (hasMore && !loading && !refreshing) {
fetchData(page + 1, false);
}
}, [hasMore, loading, refreshing, page, fetchData]);
// Reset & fetch ulang saat search atau deps berubah
useEffect(() => {
if (fetchRef.current) return; // hindari double initial
fetchRef.current = true;
setPage(1);
setData([]);
setHasMore(true);
fetchData(1, true);
}, [search, ...dependencies]);
return {
data,
loading,
refreshing,
hasMore,
search,
setSearch,
onRefresh,
loadMore,
};
};

View File

@@ -401,7 +401,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = HIPMIBadungConnect/HIPMIBadungConnect.entitlements;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_TEAM = BMY6GT6W3D;
ENABLE_BITCODE = NO;
GCC_PREPROCESSOR_DEFINITIONS = (
@@ -422,7 +422,7 @@
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_DEBUG";
PRODUCT_BUNDLE_IDENTIFIER = "com.anonymous.hipmi-mobile";
PRODUCT_NAME = HIPMIBadungConnect;
PRODUCT_NAME = "HIPMIBadungConnect";
SWIFT_OBJC_BRIDGING_HEADER = "HIPMIBadungConnect/HIPMIBadungConnect-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@@ -438,7 +438,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = HIPMIBadungConnect/HIPMIBadungConnect.entitlements;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 3;
DEVELOPMENT_TEAM = BMY6GT6W3D;
INFOPLIST_FILE = HIPMIBadungConnect/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 15.1;
@@ -454,7 +454,7 @@
);
OTHER_SWIFT_FLAGS = "$(inherited) -D EXPO_CONFIGURATION_RELEASE";
PRODUCT_BUNDLE_IDENTIFIER = "com.anonymous.hipmi-mobile";
PRODUCT_NAME = HIPMIBadungConnect;
PRODUCT_NAME = "HIPMIBadungConnect";
SWIFT_OBJC_BRIDGING_HEADER = "HIPMIBadungConnect/HIPMIBadungConnect-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";

View File

@@ -39,7 +39,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>8</string>
<string>14</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSMinimumSystemVersion</key>
@@ -55,10 +55,16 @@
</dict>
<key>NSCameraUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your camera</string>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your location</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your location</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Aplikasi membutuhkan akses lokasi untuk menampilkan peta.</string>
<key>NSMicrophoneUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your microphone</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Allow $(PRODUCT_NAME) to access your photos</string>
<string>Untuk mengunggah dokumen dan media bisnis seperti foto profil, logo usaha, poster lowongan, atau bukti transaksi di berbagai fitur aplikasi: Profile, Portofolio, Job Vacancy, Investasi, dan Donasi.</string>
<key>NSUserActivityTypes</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER).expo.index_route</string>

View File

@@ -33,6 +33,13 @@ target 'HIPMIBadungConnect' do
use_frameworks! :linkage => podfile_properties['ios.useFrameworks'].to_sym if podfile_properties['ios.useFrameworks']
use_frameworks! :linkage => ENV['USE_FRAMEWORKS'].to_sym if ENV['USE_FRAMEWORKS']
# @generated begin pre_installer - expo prebuild (DO NOT MODIFY) sync-c8812095000d6054b846ce74840f0ffb540c2757
pre_install do |installer|
# @generated begin @rnmapbox/maps-pre_installer - expo prebuild (DO NOT MODIFY) sync-ea4905840bf9fcea0acc62e92aa2e784f9d760f8
$RNMapboxMaps.pre_install(installer)
# @generated end @rnmapbox/maps-pre_installer
end
# @generated end pre_installer
use_react_native!(
:path => config[:reactNativePath],
:hermes_enabled => podfile_properties['expo.jsEngine'] == nil || podfile_properties['expo.jsEngine'] == 'hermes',
@@ -42,6 +49,9 @@ target 'HIPMIBadungConnect' do
)
post_install do |installer|
# @generated begin @rnmapbox/maps-post_installer - expo prebuild (DO NOT MODIFY) sync-c4e8f90e96f6b6c6ea9241dd7b52ab5f57f7bf36
$RNMapboxMaps.post_install(installer)
# @generated end @rnmapbox/maps-post_installer
react_native_post_install(
installer,
config[:reactNativePath],

View File

@@ -36,6 +36,7 @@
"expo-haptics": "~15.0.7",
"expo-image": "~3.0.8",
"expo-image-picker": "~17.0.8",
"expo-linear-gradient": "~15.0.7",
"expo-linking": "~8.0.8",
"expo-location": "~19.0.7",
"expo-notifications": "^0.32.13",
@@ -46,6 +47,7 @@
"expo-system-ui": "~6.0.7",
"expo-web-browser": "~15.0.9",
"lodash": "^4.17.21",
"moti": "^0.30.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.4",

View File

@@ -1,3 +1,4 @@
import { NewWrapper } from "@/components";
import ButtonCustom from "@/components/Button/ButtonCustom";
import Spacing from "@/components/_ShareComponent/Spacing";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
@@ -5,9 +6,11 @@ import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import { apiVersion } from "@/service/api-config";
import { GStyles } from "@/styles/global-styles";
import { Redirect, router } from "expo-router";
import versionBadge from "@/utils/viersionBadge";
import VersionBadge from "@/utils/viersionBadge";
import { Redirect } from "expo-router";
import { useEffect, useState } from "react";
import { Text, View } from "react-native";
import { RefreshControl, Text, View } from "react-native";
import PhoneInput, { ICountry } from "react-native-international-phone-number";
import Toast from "react-native-toast-message";
@@ -16,6 +19,7 @@ export default function LoginView() {
const [selectedCountry, setSelectedCountry] = useState<null | ICountry>(null);
const [inputValue, setInputValue] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
const [refreshing, setRefreshing] = useState<boolean>(false);
const { loginWithNomor, token, isAdmin, isUserActive } = useAuth();
@@ -25,7 +29,18 @@ export default function LoginView() {
async function onLoadVersion() {
const res = await apiVersion();
setVersion(res.data);
if (res.success) {
setVersion(versionBadge());
}
}
async function handleRefresh() {
setRefreshing(true);
await onLoadVersion();
setInputValue("");
setLoading(false);
setRefreshing(false);
}
function handleInputValue(phoneNumber: string) {
@@ -66,21 +81,13 @@ export default function LoginView() {
if (!isValid) return;
const callingCode = selectedCountry?.callingCode.replace(/^\+/, "") || "";
const fixNumber = inputValue.replace(/\s+/g, "");
let fixNumber = inputValue.replace(/\s+/g, "").replace(/^0+/, "");
const realNumber = callingCode + fixNumber;
try {
setLoading(true);
// const response = await apiLogin({ nomor: realNumber });
await loginWithNomor(realNumber);
Toast.show({
type: "success",
text1: "Sukses",
text2: "Kode OTP berhasil dikirim",
});
router.navigate(`/verification?nomor=${realNumber}`);
} catch (error) {
console.log("Error login", error);
Toast.show({
@@ -91,6 +98,30 @@ export default function LoginView() {
} finally {
setLoading(false);
}
// try {
// setLoading(true);
// // const response = await apiLogin({ nomor: realNumber });
// const response = await loginWithNomor(realNumber);
// console.log("[RESPONSE]", response);
// Toast.show({
// type: "success",
// text1: "Sukses",
// text2: "Kode OTP berhasil dikirim",
// });
// // router.navigate(`/verification?nomor=${realNumber}`);
// } catch (error) {
// console.log("Error login", error);
// Toast.show({
// type: "error",
// text1: "Error",
// text2: error as string,
// });
// } finally {
// setLoading(false);
// }
}
if (token && token !== "" && !isUserActive) {
@@ -102,11 +133,20 @@ export default function LoginView() {
}
if (token && token !== "" && isAdmin) {
return <Redirect href={"/(application)/admin/dashboard"} />;
// Akan di aktifkan jika sudah losos review
// return <Redirect href={"/(application)/admin/dashboard"} />;
// Sementara gunakan ini
return <Redirect href={"/(application)/(user)/home"} />;
}
return (
<ViewWrapper withBackground>
<NewWrapper
withBackground
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
}
>
<View style={GStyles.authContainer}>
<View>
<View style={GStyles.authContainerTitle}>
@@ -150,6 +190,6 @@ export default function LoginView() {
Coba
</ButtonCustom> */}
</View>
</ViewWrapper>
</NewWrapper>
);
}

View File

@@ -1,10 +1,13 @@
import { CheckboxCustom } from "@/components";
import Spacing from "@/components/_ShareComponent/Spacing";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import ButtonCustom from "@/components/Button/ButtonCustom";
import TextInputCustom from "@/components/TextInput/TextInputCustom";
import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import { BASE_URL } from "@/service/api-config";
import { GStyles } from "@/styles/global-styles";
import { openBrowser } from "@/utils/openBrower";
import { MaterialCommunityIcons } from "@expo/vector-icons";
import { useLocalSearchParams } from "expo-router";
import { useState } from "react";
@@ -14,7 +17,8 @@ import Toast from "react-native-toast-message";
export default function RegisterView() {
const { nomor } = useLocalSearchParams();
const [username, setUsername] = useState("");
const [term, setTerm] = useState(false);
const url = BASE_URL;
const { registerUser, isLoading } = useAuth();
const validasiData = () => {
@@ -61,6 +65,7 @@ export default function RegisterView() {
await registerUser({
nomor: nomor as string,
username: usernameLower,
termsOfServiceAccepted: term,
});
}
@@ -96,7 +101,39 @@ export default function RegisterView() {
}
/>
<ButtonCustom isLoading={isLoading} onPress={handleRegister}>
<View
style={{
flexDirection: "row",
alignItems: "center",
marginTop: 16,
marginBottom: 16,
}}
>
<CheckboxCustom value={term} onChange={() => setTerm(!term)} />
<Text style={GStyles.textLabel}>
Saya setuju dengan{" "}
<Text
style={{
color: MainColor.yellow,
textDecorationLine: "underline",
}}
onPress={() => {
const toUrl = `${url}/terms-of-service.html`;
openBrowser(toUrl);
}}
>
Syarat & Ketentuan
</Text>{" "}
yang melarang konten tidak pantas dan perilaku merugikan.
</Text>
</View>
<ButtonCustom
disabled={!term}
isLoading={isLoading}
onPress={handleRegister}
>
Daftar
</ButtonCustom>
</View>

View File

@@ -17,6 +17,8 @@ import Toast from "react-native-toast-message";
export default function VerificationView() {
const { nomor } = useLocalSearchParams<{ nomor: string }>();
console.log("[NOMOR]", nomor);
const [inputOtp, setInputOtp] = useState<string>("");
const [userNumber, setUserNumber] = useState<string>("");
const [loading, setLoading] = useState<boolean>(false);
@@ -52,7 +54,7 @@ export default function VerificationView() {
try {
const response = await apiCheckCodeOtp({ kodeId });
console.log(
"Response check code otp >>",
"[OTP] >>",
JSON.stringify(response.otp, null, 2)
);
// Kita tidak perlu simpan codeOtp di state karena verifikasi dilakukan di backend
@@ -89,8 +91,9 @@ export default function VerificationView() {
// ✅ VERIFIKASI OTOMATIS UNTUK APPLE REVIEW
if (inputOtp === "1234") {
try {
const response = await validateOtp(nomor as string);
router.replace(response);
await validateOtp(nomor as string);
return;
} catch (error) {
console.log("Error verification", error);
Toast.show({ type: "error", text1: "Gagal verifikasi" });
@@ -103,16 +106,8 @@ export default function VerificationView() {
// 🔁 VERIFIKASI NORMAL (untuk pengguna sungguhan)
try {
const response = await validateOtp(nomor as string);
// registerForPushNotificationsAsync().then((token) => {
// if (token) {
// console.log("Expo Push Token:", token);
// // TODO: Kirim token ke backend kamu
// } else {
// console.log("Failed to get Expo Push Token");
// }
// });
router.replace(response);
await validateOtp(nomor as string);
return
} catch (error) {
console.log("Error verification", error);
Toast.show({ type: "error", text1: "Gagal verifikasi" });

View File

@@ -25,7 +25,7 @@ function Collaboration_BoxPublishSection({
name={data?.Author?.username || "Username"}
rightComponent={rightComponentAvatar}
avatar={data?.Author?.Profile?.imageId}
withBottomLine
// withBottomLine
/>
<StackCustom style={{paddingBlock: 10}}>

View File

@@ -50,7 +50,7 @@ export default function Event_BoxDetailPublishSection({
<Grid.Col span={4}>
<TextCustom bold>{item.title}</TextCustom>
</Grid.Col>
<Grid.Col span={8}>
<Grid.Col span={8} style={{ paddingLeft: 10 }}>
<TextCustom>{item.value}</TextCustom>
</Grid.Col>
</Grid>

View File

@@ -22,6 +22,7 @@ const drawerItemsForumBerandaForAuthor = ({
),
label: "Edit posting",
path: `/forum/${id}/edit`,
values: "edit"
},
{
icon:
@@ -34,6 +35,7 @@ const drawerItemsForumBerandaForAuthor = ({
label: status === "Open" ? "Tutup forum" : "Buka forum",
path: "",
color: status === "Open" ? MainColor.orange : MainColor.green,
value: "status"
},
{
icon: (
@@ -42,10 +44,11 @@ const drawerItemsForumBerandaForAuthor = ({
label: "Hapus",
path: "",
color: MainColor.red,
value: "delete"
},
];
const drawerItemsForumBerandaForNonAuthor = ({ id }: { id: string }) => [
const drawerItemsForumBerandaForNonAuthor = ({ id, username }: { id: string; username: string }) => [
{
icon: (
<Ionicons name="flag" size={ICON_SIZE_SMALL} color={MainColor.white} />
@@ -53,6 +56,16 @@ const drawerItemsForumBerandaForNonAuthor = ({ id }: { id: string }) => [
label: "Laporkan diskusi",
// color: MainColor.white,
path: `/forum/${id}/report-posting`,
value: "report"
},
{
icon: (
<Ionicons name="ban" size={ICON_SIZE_SMALL} color={MainColor.white} />
),
label: `Blockir @${username}`,
color: MainColor.red,
path: `/forum/${id}/report-posting`,
value: "block"
},
];
@@ -64,6 +77,7 @@ const drawerItemsForumComentarForAuthor = ({ id }: { id: string }) => [
label: "Hapus",
color: MainColor.red,
path: "",
value: "delete"
},
];
@@ -75,5 +89,6 @@ const drawerItemsForumComentarForNonAuthor = ({ id }: { id: string }) => [
label: "Laporkan",
// color: MainColor.white,
path: `/forum/${id}/report-commentar`,
value: "report"
},
];

View File

@@ -9,15 +9,18 @@ import {
import { useAuth } from "@/hooks/use-auth";
import { apiForumDelete } from "@/service/api-client/api-forum";
import Toast from "react-native-toast-message";
import { apiForumBlockUser } from "@/service/api-client/api-user";
export default function Forum_MenuDrawerBerandaSection({
id,
authorUsername,
status,
setIsDrawerOpen,
authorId,
handlerUpdateStatus,
}: {
id: string;
authorUsername: string;
status: string;
setIsDrawerOpen: (value: boolean) => void;
authorId: string;
@@ -25,7 +28,7 @@ export default function Forum_MenuDrawerBerandaSection({
}) {
const { user } = useAuth();
const handlePress = (item: IMenuDrawerItem) => {
if (item.label === "Hapus") {
if (item.value === "delete") {
AlertDefaultSystem({
title: "Hapus diskusi",
message: "Apakah Anda yakin ingin menghapus diskusi ini?",
@@ -34,7 +37,7 @@ export default function Forum_MenuDrawerBerandaSection({
onPressRight: async () => {
try {
const response = await apiForumDelete({ id });
if (response.success) {
Toast.show({
type: "success",
@@ -52,14 +55,46 @@ export default function Forum_MenuDrawerBerandaSection({
}
},
});
} else if (item.label === "Buka forum" || item.label === "Tutup forum") {
} else if (item.value === "status") {
AlertDefaultSystem({
title: "Ubah Status",
message: "Apakah Anda yakin ingin mengubah status forum ini?",
textLeft: "Batal",
textRight: "Ubah",
onPressRight: () => {
handlerUpdateStatus?.(item.label === "Buka forum" ? "Open" : "Closed");
handlerUpdateStatus?.(
item.label === "Buka forum" ? "Open" : "Closed"
);
},
});
} else if (item.value === "block") {
AlertDefaultSystem({
title: "Blockir user",
message: `Apakah anda yakin ingin blockir user @${authorUsername}?`,
textLeft: "Batal",
textRight: "Blockir",
onPressRight: async () => {
console.log("Blockir");
const response = await apiForumBlockUser({
data: {
menuFeature: "Forum",
blockedId: authorId,
blockerId: user?.id || "",
},
});
if (response.success) {
Toast.show({
type: "success",
text1: "Berhasil blokir",
});
router.back();
} else {
Toast.show({
type: "error",
text1: response.message,
});
}
},
});
} else {
@@ -76,9 +111,12 @@ export default function Forum_MenuDrawerBerandaSection({
data={
authorId === user?.id
? drawerItemsForumBerandaForAuthor({ id, status })
: drawerItemsForumBerandaForNonAuthor({ id })
: drawerItemsForumBerandaForNonAuthor({
id,
username: authorUsername,
})
}
columns={4} // Ubah ke 2 jika ingin 2 kolom per baris
columns={authorId === user?.id ? 4 : 2} // Ubah ke 2 jika ingin 2 kolom per baris
onPressItem={handlePress as any}
/>
</>

View File

@@ -0,0 +1,108 @@
import {
BackButton,
AvatarComp,
ViewWrapper,
SearchInput,
FloatingButton,
LoaderCustom,
TextCustom,
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { apiForumGetAll } from "@/service/api-client/api-forum";
import { apiUser } from "@/service/api-client/api-user";
import { Stack, router } from "expo-router";
import _ from "lodash";
import { useState, useEffect } from "react";
import { RefreshControl } from "react-native";
import Forum_BoxDetailSection from "./DiscussionBoxSection";
export default function Forum_ViewBeranda() {
const { user } = useAuth();
const [dataUser, setDataUser] = useState<any>();
const [listData, setListData] = useState<any[]>();
const [loadingGetList, setLoadingGetList] = useState(false);
const [search, setSearch] = useState("");
useEffect(() => {
onLoadData();
onLoadDataProfile(user?.id as string);
}, [user?.id, search]);
const onLoadDataProfile = async (id: string) => {
const response = await apiUser(id);
setDataUser(response.data);
};
const onLoadData = async () => {
try {
setLoadingGetList(true);
const response = await apiForumGetAll({
category: "beranda",
search: search,
userLoginId: user?.id,
});
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetList(false);
}
};
return (
<>
<Stack.Screen
options={{
title: "Forum",
headerLeft: () => <BackButton />,
headerRight: () => (
<AvatarComp
fileId={dataUser?.Profile?.imageId}
size="base"
href={`/forum/${user?.id}/forumku`}
/>
),
}}
/>
<ViewWrapper
headerComponent={
<SearchInput
placeholder="Cari topik diskusi"
onChangeText={(e) => setSearch(e)}
/>
}
floatingButton={
<FloatingButton
onPress={() =>
router.navigate("/(application)/(user)/forum/create")
}
/>
}
refreshControl={
<RefreshControl refreshing={loadingGetList} onRefresh={onLoadData} />
}
>
{loadingGetList ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center" color="gray">
Tidak ada diskusi
</TextCustom>
) : (
listData?.map((e: any, i: number) => (
<Forum_BoxDetailSection
key={i}
data={e}
onSetData={() => {}}
isTruncate={true}
href={`/forum/${e.id}`}
isRightComponent={false}
/>
))
)}
</ViewWrapper>
</>
);
}

View File

@@ -0,0 +1,214 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AvatarComp,
BackButton,
FloatingButton,
LoaderCustom,
SearchInput,
StackCustom,
TextCustom, // ← gunakan NewWrapper yang sudah diperbaiki
} from "@/components";
import SkeletonCustom from "@/components/_ShareComponent/SkeletonCustom";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import { useAuth } from "@/hooks/use-auth";
import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection";
import { apiForumGetAll } from "@/service/api-client/api-forum";
import { apiUser } from "@/service/api-client/api-user";
import { router, Stack } from "expo-router";
import _ from "lodash";
import { useCallback, useEffect, useState } from "react";
import { RefreshControl, View } from "react-native";
import { MainColor } from "@/constants/color-palet";
// Sesuai dengan `takeData = 5` di API-mu
const PAGE_SIZE = 5;
export default function Forum_ViewBeranda2() {
const { user } = useAuth();
const [dataUser, setDataUser] = useState<any>(null);
const [listData, setListData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [page, setPage] = useState(1);
const [search, setSearch] = useState("");
// 🔹 Load data profil user sekali
useEffect(() => {
if (user?.id) {
apiUser(user.id).then((res) => setDataUser(res.data));
}
}, [user?.id]);
// 🔹 Reset dan muat ulang saat search atau user berubah
useEffect(() => {
setPage(1);
setListData([]);
setHasMore(true);
fetchData(1, true);
}, [search, user?.id]);
// 🔹 Fungsi fetch data
const fetchData = async (pageNumber: number, clear: boolean) => {
if (!user?.id) return;
// Cegah multiple call
if (!clear && (loading || refreshing)) return;
const isRefresh = clear;
if (isRefresh) setRefreshing(true);
if (!isRefresh) setLoading(true);
try {
const response = await apiForumGetAll({
category: "beranda",
search: search || "",
userLoginId: user.id,
page: String(pageNumber), // API terima string
});
const newData = response.data || [];
setListData((prev) => {
const current = Array.isArray(prev) ? prev : [];
return clear ? newData : [...current, ...newData];
});
setHasMore(newData.length === PAGE_SIZE);
setPage(pageNumber);
} catch (error) {
console.error("[ERROR] Fetch forum:", error);
setHasMore(false);
} finally {
setRefreshing(false);
setLoading(false);
}
};
// 🔹 Pull-to-refresh
const onRefresh = useCallback(() => {
fetchData(1, true);
}, [search, user?.id]);
// 🔹 Infinite scroll
const loadMore = useCallback(() => {
if (hasMore && !loading && !refreshing) {
fetchData(page + 1, false);
}
}, [hasMore, loading, refreshing, page, search, user?.id]);
// 🔹 Render item forum
const renderForumItem = ({ item }: { item: any }) => (
<Forum_BoxDetailSection
key={item.id}
data={item}
onSetData={() => {}}
isTruncate={true}
href={`/forum/${item.id}`}
isRightComponent={false}
/>
);
// 🔹 Komponen Header List (di dalam FlatList)
const ListHeaderComponent = (
<View style={{ paddingVertical: 8, alignItems: "center" }}>
<TextCustom>Diskusi Terbaru</TextCustom>
</View>
);
// 🔹 Komponen Footer List (loading indicator)
const ListFooterComponent =
loading && !refreshing && listData.length > 0 ? (
<View style={{ paddingVertical: 16, alignItems: "center" }}>
{/* <Text style={{ color: "#aaa", fontSize: 14 }}>Memuat diskusi...</Text> */}
<LoaderCustom />
</View>
) : null;
// Skeleton List (untuk initial load)
const SkeletonListComponent = () => (
<View style={{ flex: 1 }}>
<StackCustom>
{Array.from({ length: 5 }).map((_, i) => (
<SkeletonCustom height={200} key={i} />
))}
</StackCustom>
</View>
);
// Komponen Empty
const EmptyComponent = () => (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: 20,
}}
>
<TextCustom align="center" color="gray">
{search ? "Tidak ada hasil pencarian" : "Tidak ada diskusi"}
</TextCustom>
</View>
);
return (
<>
{/* 🔹 Header Navigation */}
<Stack.Screen
options={{
title: "Forum",
headerLeft: () => <BackButton />,
headerRight: () => (
<AvatarComp
fileId={dataUser?.Profile?.imageId}
size="base"
href={`/forum/${user?.id}/forumku`}
/>
),
}}
/>
{/* 🔹 NewWrapper dalam mode list */}
<NewWrapper
// Header global (di atas FlatList, sticky)
headerComponent={
<View style={{ paddingHorizontal: 16, paddingTop: 8 }}>
<SearchInput
placeholder="Cari topik diskusi"
onChangeText={_.debounce((text) => setSearch(text), 500)}
// value={search}
/>
</View>
}
// Floating action button
floatingButton={
<FloatingButton
onPress={() =>
router.navigate("/(application)/(user)/forum/create")
}
/>
}
// --- Mode List Props ---
listData={listData}
renderItem={renderForumItem}
refreshControl={
<RefreshControl
// IOS
tintColor={MainColor.yellow}
// Android
colors={[MainColor.yellow]}
progressBackgroundColor={MainColor.yellow}
refreshing={refreshing}
onRefresh={onRefresh}
/>
}
onEndReached={loadMore}
ListFooterComponent={ListFooterComponent}
ListEmptyComponent={
loading && _.isEmpty(listData) ? <SkeletonListComponent /> : <EmptyComponent />
}
// ------------------------
/>
</>
);
}

View File

@@ -0,0 +1,144 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AvatarComp,
ButtonCustom,
CenterCustom,
DrawerCustom,
FloatingButton,
Grid,
LoaderCustom,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection";
import Forum_MenuDrawerBerandaSection from "@/screens/Forum/MenuDrawerSection.tsx/MenuBeranda";
import { apiForumGetAll } from "@/service/api-client/api-forum";
import { apiUser } from "@/service/api-client/api-user";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function View_Forumku() {
const { id } = useLocalSearchParams();
const { user } = useAuth();
const [openDrawer, setOpenDrawer] = useState(false);
const [status, setStatus] = useState("");
const [listData, setListData] = useState<any | null>(null);
const [dataUser, setDataUser] = useState<any | null>(null);
const [loadingGetList, setLoadingGetList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
onLoadDataProfile(id as string);
}, [id])
);
const onLoadDataProfile = async (id: string) => {
try {
const response = await apiUser(id);
setDataUser(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
}
};
const onLoadData = async () => {
try {
setLoadingGetList(true);
const response = await apiForumGetAll({
search: "",
authorId: id as string,
category: "forumku",
});
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetList(false);
}
};
return (
<>
<ViewWrapper
floatingButton={
user?.id === id && (
<FloatingButton
onPress={() =>
router.navigate("/(application)/(user)/forum/create")
}
/>
)
}
>
<StackCustom>
<CenterCustom>
<AvatarComp
fileId={dataUser?.Profile?.imageId}
href={`/(application)/(image)/preview-image/${dataUser?.Profile?.imageId}`}
size="xl"
/>
</CenterCustom>
<Grid>
<Grid.Col span={6}>
<TextCustom bold truncate>
@{dataUser?.username || "-"}
</TextCustom>
<TextCustom>{listData?.length || "0"} postingan</TextCustom>
</Grid.Col>
<Grid.Col span={6} style={{ alignItems: "flex-end" }}>
<ButtonCustom href={`/profile/${dataUser?.Profile?.id}`}>
Kunjungi Profile
</ButtonCustom>
</Grid.Col>
</Grid>
{loadingGetList ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom> Tidak ada diskusi</TextCustom>
) : (
<>
{listData?.map((item: any, index: number) => (
<Forum_BoxDetailSection
isRightComponent={false}
key={index}
data={item}
isTruncate={true}
href={`/forum/${item.id}`}
onSetData={(value) => {
setOpenDrawer(value.setOpenDrawer);
setStatus(value.setStatus);
}}
/>
))}
</>
)}
</StackCustom>
</ViewWrapper>
{/* Drawer Komponen Eksternal */}
<DrawerCustom
height={"auto"}
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
>
<Forum_MenuDrawerBerandaSection
id={id as string}
status={status}
setIsDrawerOpen={() => {
setOpenDrawer(false);
}}
authorId={id as string}
authorUsername={dataUser?.username}
/>
</DrawerCustom>
</>
);
}

View File

@@ -0,0 +1,215 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AvatarComp,
ButtonCustom,
CenterCustom,
FloatingButton,
Grid,
LoaderCustom,
Spacing,
StackCustom,
TextCustom,
} from "@/components";
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
import NoDataText from "@/components/_ShareComponent/NoDataText";
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection";
import { apiForumGetAll } from "@/service/api-client/api-forum";
import { apiUser } from "@/service/api-client/api-user";
import { router, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useEffect, useState } from "react";
import { RefreshControl, View } from "react-native";
const PAGE_SIZE = 5;
export default function View_Forumku2() {
const { id } = useLocalSearchParams();
const { user } = useAuth();
const [listData, setListData] = useState<any[]>([]);
const [dataUser, setDataUser] = useState<any | null>(null);
const [loading, setLoading] = useState(false);
const [refreshing, setRefreshing] = useState(false);
const [hasMore, setHasMore] = useState(true);
const [page, setPage] = useState(1);
const [count, setCount] = useState(0);
useEffect(() => {
onLoadDataProfile(id as string);
}, [id]);
useEffect(() => {
setPage(1);
setListData([]);
setHasMore(true);
fetchData(1, true);
}, [user?.id]);
const onLoadDataProfile = async (id: string) => {
try {
const response = await apiUser(id);
setDataUser(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
}
};
// 🔹 Fungsi fetch data
const fetchData = async (pageNumber: number, clear: boolean) => {
if (!user?.id) return;
// Cegah multiple call
if (!clear && (loading || refreshing)) return;
const isRefresh = clear;
if (isRefresh) setRefreshing(true);
if (!isRefresh) setLoading(true);
try {
const response = await apiForumGetAll({
category: "forumku",
authorId: id as string,
userLoginId: user.id,
page: String(pageNumber), // API terima string
});
const newData = response.data.data || [];
setListData((prev) => {
const current = Array.isArray(prev) ? prev : [];
return clear ? newData : [...current, ...newData];
});
setHasMore(newData.length === PAGE_SIZE);
setPage(pageNumber);
setCount(response.data.count);
} catch (error) {
console.error("[ERROR] Fetch forum:", error);
setHasMore(false);
} finally {
setRefreshing(false);
setLoading(false);
}
};
// 🔹 Pull-to-refresh
const onRefresh = useCallback(() => {
fetchData(1, true);
}, [user?.id]);
// 🔹 Infinite scroll
const loadMore = useCallback(() => {
if (hasMore && !loading && !refreshing) {
fetchData(page + 1, false);
}
}, [hasMore, loading, refreshing, page, user?.id]);
const randerHeaderComponent = () => (
<>
<CenterCustom>
<AvatarComp
fileId={dataUser?.Profile?.imageId}
href={`/(application)/(image)/preview-image/${dataUser?.Profile?.imageId}`}
size="xl"
/>
</CenterCustom>
<Grid>
<Grid.Col style={{ paddingLeft: 8 }} span={6}>
<TextCustom bold truncate>
@{dataUser?.username || "-"}
</TextCustom>
<TextCustom>{count || "0"} postingan</TextCustom>
</Grid.Col>
<Grid.Col span={6} style={{ alignItems: "flex-end", paddingRight: 8 }}>
<ButtonCustom href={`/profile/${dataUser?.Profile?.id}`}>
Kunjungi Profile
</ButtonCustom>
</Grid.Col>
</Grid>
<Spacing />
</>
);
const renderList = ({ item }: { item: any }) => (
<Forum_BoxDetailSection
key={item.id}
data={item}
onSetData={() => {}}
isTruncate={true}
href={`/forum/${item.id}`}
isRightComponent={false}
/>
);
// Skeleton List (untuk initial load)
const SkeletonListComponent = () => (
<View style={{ flex: 1 }}>
<StackCustom>
{Array.from({ length: 5 }).map((_, i) => (
<CustomSkeleton height={200} key={i} />
))}
</StackCustom>
</View>
);
// Komponen Empty
const EmptyComponent = () => (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: 20,
}}
>
<NoDataText />
</View>
);
// 🔹 Komponen Footer List (loading indicator)
const ListFooterComponent =
loading && !refreshing && listData.length > 0 ? (
<View style={{ paddingVertical: 16, alignItems: "center" }}>
{/* <Text style={{ color: "#aaa", fontSize: 14 }}>Memuat diskusi...</Text> */}
<LoaderCustom />
</View>
) : null;
return (
<>
<NewWrapper
floatingButton={
user?.id === id && (
<FloatingButton
onPress={() =>
router.navigate("/(application)/(user)/forum/create")
}
/>
)
}
listData={listData}
renderItem={renderList}
refreshControl={
<RefreshControl
// IOS
tintColor={MainColor.yellow}
// Android
colors={[MainColor.yellow]}
progressBackgroundColor={MainColor.yellow}
refreshing={refreshing}
onRefresh={onRefresh}
/>
}
onEndReached={loadMore}
ListHeaderComponent={randerHeaderComponent()}
ListFooterComponent={ListFooterComponent}
ListEmptyComponent={
loading && _.isEmpty(listData) ? <SkeletonListComponent /> : <EmptyComponent />
}
/>
</>
);
}

View File

@@ -59,11 +59,30 @@ export const stylesHome = StyleSheet.create({
borderWidth: 2,
borderColor: AccentColor.blue,
},
gridItemInactive: {
width: "46%",
height: "100%",
aspectRatio: 1,
backgroundColor: MainColor.darkblue,
borderRadius: 8,
padding: 16,
alignItems: "center",
justifyContent: "center",
marginVertical: 8,
borderWidth: 2,
borderColor: AccentColor.blue,
opacity: 0.7,
},
gridLabel: {
marginTop: 8,
color: "white",
fontWeight: "bold",
},
gridLabelInactive: {
marginTop: 8,
color: "gray",
fontWeight: "bold",
},
jobVacancyContainer: {
backgroundColor: MainColor.darkblue,
borderRadius: 8,

View File

@@ -1,12 +1,12 @@
import { ITabs } from "@/components/_Interface/types";
export const tabsHome: any = (profileId: string) => [
export const tabsHome: any = ({acceptedForumTermsAt, profileId}: {acceptedForumTermsAt: Date, profileId: string}) => [
{
id: "forum",
icon: "chatbubble-ellipses-outline",
activeIcon: "chatbubble-ellipses",
label: "Forum",
path: "/forum",
path: acceptedForumTermsAt ? "/forum" : "/forum/terms",
isActive: true,
disabled: false,
},

View File

@@ -9,21 +9,25 @@ export default function Home_FeatureSection() {
name: "Event",
icon: <Ionicons name="analytics" size={48} color="white" />,
onPress: () => router.push("/(application)/(user)/event/(tabs)"),
status: "active",
},
{
name: "Collaboration",
icon: <Ionicons name="share" size={48} color="white" />,
icon: <Ionicons name="share" size={48} color="gray" />,
onPress: () => router.push("/(application)/(user)/collaboration/(tabs)"),
status: "inactive",
},
{
name: "Voting",
icon: <Ionicons name="cube" size={48} color="white" />,
onPress: () => router.push("/(application)/(user)/voting/(tabs)"),
status: "active",
},
{
name: "Crowdfunding",
icon: <Ionicons name="heart" size={48} color="white" />,
onPress: () => router.push("/(application)/(user)/crowdfunding"),
status: "active",
},
];
@@ -33,11 +37,12 @@ export default function Home_FeatureSection() {
{listFeature.map((item, index) => (
<TouchableOpacity
key={index}
style={stylesHome.gridItem}
style={item.status === "inactive" ? stylesHome.gridItemInactive : stylesHome.gridItem}
onPress={item.onPress}
disabled={item.status === "inactive"}
>
{item.icon}
<Text style={stylesHome.gridLabel}>{item.name}</Text>
<Text style={item.status === "inactive" ? stylesHome.gridLabelInactive : stylesHome.gridLabel}>{item.name}</Text>
</TouchableOpacity>
))}
</View>

View File

@@ -1,12 +1,13 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
BaseBox,
Grid,
ProgressCustom,
StackCustom,
TextCustom,
BaseBox,
Grid,
ProgressCustom,
StackCustom,
TextCustom,
} from "@/components";
import API_STRORAGE from "@/constants/base-url-api-strorage";
import { MainColor } from "@/constants/color-palet";
import DUMMY_IMAGE from "@/constants/dummy-image-value";
import { countDownAndCondition } from "@/utils/countDownAndCondition";
import { Ionicons } from "@expo/vector-icons";
@@ -21,7 +22,7 @@ export default function Investment_BoxBerandaSection({
id: string;
data: any;
}) {
// console.log("[DATA By one]", JSON.stringify(data, null, 2));
// console.log("[DATA By one]", JSON.stringify(data, null, 2));
const [value, setValue] = useState({
sisa: 0,
@@ -32,6 +33,8 @@ export default function Investment_BoxBerandaSection({
updateCountDown();
}, [data]);
console.log("[DATA BERANDA]", JSON.stringify(data, null, 2));
const updateCountDown = () => {
const countDown = countDownAndCondition({
duration: data?.pencarianInvestor,
@@ -66,8 +69,10 @@ export default function Investment_BoxBerandaSection({
<TextCustom truncate={2}>{data.title}</TextCustom>
<ProgressCustom
label={`${data.progress}%`}
value={data.progress}
value={Number(data.progress)}
size="lg"
animated
color="primary"
/>
{value.reminder ? (
<View
@@ -79,13 +84,11 @@ export default function Investment_BoxBerandaSection({
>
<Ionicons name="alert-circle-outline" size={16} color="red" />
<TextCustom truncate color="red" size="small">
Periode Investasi Berakhir
Periode Berakhir
</TextCustom>
</View>
) : (
<TextCustom>
Sisa waktu: {value.sisa} hari
</TextCustom>
<TextCustom>Sisa waktu: {value.sisa} hari</TextCustom>
)}
</StackCustom>
</Grid.Col>

View File

@@ -4,9 +4,11 @@ import { router } from "expo-router";
export default function Investment_ButtonInvestasiSection({
id,
isMine,
reminder,
}: {
id: string;
isMine: boolean;
reminder: boolean;
}) {
return (
<>
@@ -14,11 +16,12 @@ export default function Investment_ButtonInvestasiSection({
<ButtonCustom disabled>Investasi ini milik Anda</ButtonCustom>
) : (
<ButtonCustom
disabled={reminder}
onPress={() => {
router.navigate(`/investment/${id}/(transaction-flow)`);
}}
>
Beli Saham
{reminder ? "Periode Investasi Berakhir" : "Beli Saham"}
</ButtonCustom>
)}
</>

View File

@@ -52,7 +52,7 @@ export default function Invesment_DetailDataPublishSection({
<ReportBox text={data?.catatan} />
)}
<Invesment_BoxProgressSection
progress={data?.progress}
progress={Number(data?.progress)}
status={status as string}
/>
<Invesment_BoxDetailDataSection

Some files were not shown because too many files have changed in this diff Show More