Compare commits

...

12 Commits

Author SHA1 Message Date
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
86 changed files with 2628 additions and 600 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: "13",
},
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

@@ -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

@@ -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
@@ -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

@@ -17,6 +17,8 @@ import { useEffect, useState } from "react";
export default function Application() {
const { token, user } = useAuth();
const [data, setData] = useState<any>();
console.log("[User] >>", JSON.stringify(user?.id, null, 2));
useEffect(() => {
onLoadData();

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

@@ -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

@@ -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

@@ -16,7 +16,7 @@ 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 { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import {
@@ -95,11 +95,11 @@ export default function AdminForumReportComment() {
>
<BaseBox>
<StackCustom gap={"sm"}>
<GridDetail_4_8
<GridSpan_4_8
label={<TextCustom bold>Username</TextCustom>}
value={<TextCustom>{data?.Author?.username || "-"}</TextCustom>}
/>
<GridDetail_4_8
<GridSpan_4_8
label={<TextCustom bold>Komentar</TextCustom>}
value={<TextCustom>{data?.komentar || "-"}</TextCustom>}
/>
@@ -208,20 +208,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 +231,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

@@ -17,7 +17,7 @@ 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 { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_BUTTON } from "@/constants/constans-value";
import {
@@ -95,12 +95,12 @@ export default function AdminForumReportPosting() {
>
<BaseBox>
<StackCustom gap={"sm"}>
<GridDetail_4_8
<GridSpan_4_8
label={<TextCustom bold>Username</TextCustom>}
value={<TextCustom>{data?.Author?.username || "-"}</TextCustom>}
/>
<GridDetail_4_8
<GridSpan_4_8
label={<TextCustom bold>Status</TextCustom>}
value={
data && data?.ForumMaster_StatusPosting?.status ? (
@@ -121,7 +121,7 @@ export default function AdminForumReportPosting() {
}
/>
<GridDetail_4_8
<GridSpan_4_8
label={<TextCustom bold>Postingan</TextCustom>}
value={<TextCustom>{data?.diskusi || "-"}</TextCustom>}
/>
@@ -229,20 +229,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 +252,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

@@ -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

@@ -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

@@ -74,7 +74,7 @@ export default function TermsAgreement() {
<>
<Stack.Screen
options={{
title: "Terms Agreement",
title: "Terms & Conditions",
}}
/>
<ViewWrapper footerComponent={footerComponent}>
@@ -87,6 +87,7 @@ export default function TermsAgreement() {
alignItems: "center",
marginTop: 16,
marginBottom: 16,
paddingInline: 10,
}}
>
<CheckboxCustom value={term} onChange={() => setTerm(!term)} />

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

@@ -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,
value,
}: {
label: React.ReactNode;
value: React.ReactNode;
}) => {
return (
<Grid>
<Grid.Col
span={4}
style={{
justifyContent: "flex-start",
paddingRight: 8,
paddingLeft: 8,
}}
>
{label}
</Grid.Col>
<Grid.Col span={8} style={{ justifyContent: "center", paddingRight: 8 }}>
{value}
</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

@@ -72,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 {
@@ -81,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);
@@ -104,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);
@@ -132,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: {
@@ -145,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);
}
@@ -160,9 +206,8 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
setIsLoading(true);
try {
const response = await apiRegister({ data: userData });
console.log("response", response);
console.log("[REGISTER FETCH]", JSON.stringify(response, null, 2));
const { token } = response;
if (!response.success) {
Toast.show({
type: "info",
@@ -173,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>13</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSMinimumSystemVersion</key>
@@ -53,15 +53,18 @@
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<!-- Photo Library -->
<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>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>
<!-- Camera -->
<key>NSCameraUsageDescription</key>
<string>Untuk mengambil foto langsung saat mengunggah dokumen bisnis seperti foto profil, logo usaha, poster, atau bukti pembayaran di fitur 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) {
@@ -110,7 +141,12 @@ export default function LoginView() {
}
return (
<ViewWrapper withBackground>
<NewWrapper
withBackground
refreshControl={
<RefreshControl refreshing={refreshing} onRefresh={handleRefresh} />
}
>
<View style={GStyles.authContainer}>
<View>
<View style={GStyles.authContainerTitle}>
@@ -154,6 +190,6 @@ export default function LoginView() {
Coba
</ButtonCustom> */}
</View>
</ViewWrapper>
</NewWrapper>
);
}

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

@@ -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

@@ -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

View File

@@ -5,16 +5,6 @@ import { Ionicons } from "@expo/vector-icons";
export default function Portofolio_SocialMediaSection({ data }: { data: any }) {
const listData = [
{
label: data && data?.facebook ? data.facebook : "-",
icon: (
<Ionicons
name="logo-facebook"
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
),
},
{
label: data && data?.tiktok ? data.tiktok : "-",
icon: (
@@ -35,6 +25,16 @@ export default function Portofolio_SocialMediaSection({ data }: { data: any }) {
/>
),
},
{
label: data && data?.facebook ? data.facebook : "-",
icon: (
<Ionicons
name="logo-facebook"
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
),
},
{
label: data && data?.twitter ? data.twitter : "-",
icon: (

View File

@@ -62,6 +62,18 @@ export const drawerItemsProfile = ({
path: `/(application)/portofolio/${id}/create`,
value: "create-portofolio",
},
{
icon: (
<Ionicons
name="list-circle"
size={ICON_SIZE_MEDIUM}
color={AccentColor.white}
/>
),
label: "Blocked List",
path: `/(application)/profile/${id}/blocked-list`,
value: "blocked-list",
},
{
icon: (
<Ionicons
@@ -150,6 +162,18 @@ export const drawerItemsProfile = ({
label: "Tambah portofolio",
path: `/(application)/portofolio/${id}/create`,
value: "create-portofolio",
},
{
icon: (
<Ionicons
name="list-circle"
size={ICON_SIZE_MEDIUM}
color={AccentColor.white}
/>
),
label: "Blocked List",
path: `/(application)/profile/${id}/blocked-list`,
value: "blocked-list",
},
{
icon: (

View File

@@ -22,9 +22,11 @@ export default function Voting_BoxDetailHasilVotingSection({
<Grid>
{listData?.map((item: any, i: number) => (
<Grid.Col span={12 / listData?.length} style={{ alignItems: "center" }} key={i}>
<StackCustom>
<StackCustom style={{
alignItems: "center",
}}>
<CircleContainer value={item?.jumlah} />
<TextCustom align="center" size="small">{item?.value}</TextCustom>
<TextCustom truncate={2} align="center" size="small">{item?.value}</TextCustom>
</StackCustom>
</Grid.Col>
))}

View File

@@ -167,3 +167,56 @@ export async function apiAdminMasterTypeOfEventUpdate({
}
// ================== END EVENT ================== //
// ================== START DONATION ================== //
export async function apiAdminMasterDonationCategory() {
try {
const response = await apiConfig.get(`/mobile/admin/master/donation`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiAdminMasterDonationCategoryById({ id }: { id: string }) {
try {
const response = await apiConfig.get(`/mobile/admin/master/donation/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiAdminMasterDonationCategoryUpdate({
id,
data,
}: {
id: string;
data: any;
}) {
try {
const response = await apiConfig.put(
`/mobile/admin/master/donation/${id}`,
{
data: data,
}
);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiAdminMasterDonationCategoryCreate({ data }: { data: any }) {
try {
const response = await apiConfig.post(`/mobile/admin/master/donation`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
// ================== END DONATION ================== //

View File

@@ -0,0 +1,45 @@
import { apiConfig } from "../api-config";
/**
* @param id | Profile ID
* @param search | Search Query
* @param page | Page Number
*/
export async function apiGetBlocked({
id,
search,
page,
}: {
id: string;
search?: string;
page?: string;
}) {
const pageQuery = page ? `&page=${page}` : "";
const searchQuery = search ? `&search=${search}` : "";
try {
const response = await apiConfig.get(
`/mobile/block-user?id=${id}${pageQuery}${searchQuery}`
);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiGetBlockedById({ id }: { id: string }) {
try {
const response = await apiConfig.get(`/mobile/block-user/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiUnblock({ id }: { id: string }) {
try {
const response = await apiConfig.delete(`/mobile/block-user/${id}`);
return response.data;
} catch (error) {
throw error;
}
}

View File

@@ -14,13 +14,22 @@ export async function apiForumCreate({ data }: { data: any }) {
export async function apiForumGetAll({
search,
authorId,
userLoginId,
category,
page,
}: {
search: string;
search?: string;
authorId?: string;
userLoginId?: string;
category: "beranda" | "forumku";
page?: string;
}) {
const authorQuery = authorId ? `?authorId=${authorId}` : "";
const searchQuery = search ? `?search=${search}` : "";
const query = search ? searchQuery : authorQuery;
const categoryQuery = `?category=${category}`;
const authorQuery = authorId ? `&authorId=${authorId}` : "";
const userLoginQuery = userLoginId ? `&userLoginId=${userLoginId}` : "";
const searchQuery = search ? `&search=${search}` : "";
const pageQuery = page ? `&page=${page}` : "";
const query = `${categoryQuery}${authorQuery}${userLoginQuery}${searchQuery}${pageQuery}`;
try {
const response = await apiConfig.get(`/mobile/forum${query}`);

View File

@@ -1,5 +1,17 @@
import { apiConfig } from "../api-config";
// ================== START MASTER ================== //
export async function apiMasterAppCategory() {
try {
const response = await apiConfig.get(`/mobile/master/app-category`);
return response.data;
} catch (error) {
throw error;
}
}
// ================== END MASTER ================== //
// ================== START MASTER PORTFOLIO ================== //
export async function apiMasterBidangBisnis() {
try {
@@ -167,4 +179,4 @@ export async function apiMasterTransaction() {
} catch (error) {
throw error;
}
}
}

View File

@@ -14,3 +14,25 @@ export async function apiDeleteUser({id}:{id: string}) {
const response = await apiConfig.delete(`/mobile/user/${id}`);
return response.data;
}
export async function apiForumBlockUser({
data,
}: {
data: {
// Id yang di blokir
blockedId: string;
// Id yang melakukan blokir
blockerId: string;
menuFeature: "Event" | "Forum";
};
}) {
console.log("[FETCH API]", data);
try {
const response = await apiConfig.post(`/mobile/block-user`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}

View File

@@ -79,12 +79,14 @@ export async function apiVotingUpdateData({
}
}
export async function apiVotingGetAll({ search, category, authorId }: { search?: string, category: "beranda" | "contribution" | "all-history" | "my-history", authorId?: string }) {
export async function apiVotingGetAll({ search, category, authorId, userLoginId }: { search?: string, category: "beranda" | "contribution" | "all-history" | "my-history", authorId?: string, userLoginId?: string }) {
try {
console.log("userLoginId", userLoginId);
const categoryQuery = category ? `?category=${category}` : "";
const searchQuery = search ? `&search=${search}` : "";
const authorIdQuery = authorId ? `&authorId=${authorId}` : "";
const response = await apiConfig.get(`/mobile/voting${categoryQuery}${searchQuery}${authorIdQuery}`);
const userLoginIdQuery = userLoginId ? `&userLoginId=${userLoginId}` : "";
const response = await apiConfig.get(`/mobile/voting${categoryQuery}${searchQuery}${authorIdQuery}${userLoginIdQuery}`);
return response.data;
} catch (error) {
throw error;

View File

@@ -14,13 +14,13 @@ apiConfig.interceptors.request.use(
async (config) => {
console.log("API_BASE_URL >>", API_BASE_URL);
const token = await AsyncStorage.getItem("authToken");
// console.log("[TOKEN] >>", token);
if (token) {
// config.timeout = 10000;
config.headers["Content-Type"] = "application/json";
config.headers.Authorization = `Bearer ${token}`;
}
// console.log("config", JSON.stringify(config, null, 2));
return config;
},
(error) => {
@@ -29,16 +29,15 @@ apiConfig.interceptors.request.use(
);
export async function apiVersion() {
// console.log("API_BASE_URL", API_BASE_URL);
const response = await apiConfig.get("/version");
return response.data;
}
export async function apiLogin({ nomor }: { nomor: string }) {
const response = await apiConfig.post("/auth/login", {
const response = await apiConfig.post("/auth/mobile-login", {
nomor: nomor,
});
return response.data;
return response.data;;
}
export async function apiCheckCodeOtp({ kodeId }: { kodeId: string }) {
@@ -47,7 +46,7 @@ export async function apiCheckCodeOtp({ kodeId }: { kodeId: string }) {
}
export async function apiValidationCode({ nomor }: { nomor: string }) {
const response = await apiConfig.post(`/auth/validasi`, {
const response = await apiConfig.post(`/auth/mobile-validasi`, {
nomor: nomor,
});
return response.data;
@@ -58,7 +57,7 @@ export async function apiRegister({
}: {
data: { nomor: string; username: string; termsOfServiceAccepted: boolean };
}) {
const response = await apiConfig.post(`/auth/register`, {
const response = await apiConfig.post(`/auth/mobile-register`, {
data: data,
});
return response.data;

View File

@@ -16,7 +16,7 @@ export const GStyles = StyleSheet.create({
// =============== Main Styles =============== //
container: {
flex: 1,
paddingInline: PADDING_LARGE,
paddingInline: PADDING_MEDIUM,
paddingBlock: PADDING_EXTRA_SMALL,
backgroundColor: MainColor.darkblue,
},

108
utils/badWordsIndonesia.ts Normal file
View File

@@ -0,0 +1,108 @@
// badWordsIndonesia.js (Versi Lengkap)
const badWordsIndonesia = [
// 🐶 Kata Kasar & Vulgar
'anjing', 'babi', 'bangsat', 'bodoh', 'goblok', 'idiot', 'jancok', 'jembut', 'kampret',
'kontol', 'memek', 'ngentot', 'peler', 'puki', 'sialan', 'tai', 'tolol', 'wibu',
'anjingg', 'babbii', 'bangsaat', 'gobllokk', 'jancokk', 'kontoll', 'memekk', 'ngentott',
'pelerr', 'puuki', 'sialann', 'taii', 'tololl', 'wibuu',
// 🔥 Kata Sindiran & Penghinaan
'bego', 'dungu', 'edan', 'gila', 'goblog', 'kampang', 'kampret', 'keparat', 'lonte',
'main mata', 'monyet', 'najis', 'ngeyel', 'ngibul', 'ngomong seenaknya', 'ngurangin',
'ngutang', 'ngurusin urusan orang', 'pemalas', 'pengecut', 'penipu', 'sinting',
'begoo', 'dunguu', 'goblogg', 'kampangg', 'keparatt', 'lontee', 'monyyet', 'najiss',
'ngeyell', 'ngibull', 'ngomongg seenaknya', 'nguranginn', 'ngutangg', 'pemalass',
'pengecutt', 'penipuu', 'sintting',
// ⚖️ Kata SARA & Diskriminasi
'cina', 'kafir', 'kampungan', 'kacung', 'mampus', 'menghina', 'racist', 'sara',
'setan', 'syiah', 'waria', 'wong jowo', 'wong sunda', 'wong madura',
'chinna', 'kafiir', 'kampungann', 'kacungg', 'mampuss', 'menghinna', 'racisst',
'saraa', 'setann', 'syiahh', 'wariia', 'wong jowoo', 'wong sundaa', 'wong maduraa',
// 💸 Kata Spam / Promosi Ilegal
'judi', 'togel', 'slot', 'casino', 'poker', 'qq', 'bandar', 'agen', 'link', 'wa',
'whatsapp', 'telepon', 'nomor', 'hp', 'sms', 'grup', 'join', 'daftar', 'bonus',
'deposit', 'withdraw', 'uang', 'duit', 'rp', 'ratusan', 'juta', 'milyar',
'judii', 'togell', 'slotss', 'casinoo', 'pokerr', 'qqq', 'bandarr', 'agenn', 'linkk',
'waa', 'whatsappp', 'teleponn', 'nomorr', 'hpp', 'smss', 'grupp', 'jooin', 'daftarr',
'bonuss', 'depositt', 'withdraww', 'uangs', 'duitt', 'rpp', 'ratusann', 'jutaa', 'milyarr',
// 🧩 Variasi Penulisan (Bypass Filter)
'a*njing', 'b*b*i', 'b*ngsat', 'g*blok', 'k*nt*l', 'm*m*k', 'n*g*nt*t', 'p*l*r',
't*i', 't*l*l', 'j*n*c*k', 'j*m*b*t', 'k*m*p*r*t', 's*i*l*a*n', 'w*b*u',
'a.n.j.i.n.g', 'b.a.b.i', 'b.a.n.g.s.a.t', 'g.o.b.l.o.k', 'k.o.n.t.o.l', 'm.e.m.e.k',
'n.g.e.n.t.o.t', 'p.e.l.e.r', 't.a.i', 't.o.l.o.l', 'j.a.n.c.o.k', 'j.e.m.b.u.t',
'k.a.m.p.r.e.t', 's.i.a.l.a.n', 'w.i.b.u',
// 📱 Variasi dengan Angka & Simbol
'4nj1ng', 'b4b1', 'b4ngs4t', 'g0bl0k', 'k0nt0l', 'm3m3k', 'ng3nt0t', 'p3l3r',
't4i', 't0l0l', 'j4nc0k', 'j3mbut', 'k4mpr3t', 's14l4n', 'w1bu',
'4nj1ngg', 'b4b11', 'b4ngs4tt', 'g0bl0kk', 'k0nt0ll', 'm3m3kk', 'ng3nt0tt',
'p3l3rr', 't4ii', 't0l0ll', 'j4nc0kk', 'j3mbutt', 'k4mpr3tt', 's14l4nn', 'w1buu',
// 🗣️ Kata yang Sering Digunakan dalam Konteks Negatif
'dasar', 'kamu', 'kau', 'lu', 'lo', 'gue', 'gua', 'kita', 'kami', 'mereka',
'dasarr', 'kamuu', 'kauu', 'luu', 'loo', 'guee', 'guua', 'kitaa', 'kamii', 'merekaa',
'dasar bodoh', 'dasar goblok', 'dasar bangsat', 'dasar idiot', 'dasar sialan',
'dasar bego', 'dasar dungu', 'dasar edan', 'dasar gila', 'dasar sinting',
'dasar pemalas', 'dasar pengecut', 'dasar penipu', 'dasar najis', 'dasar kampret',
// 🚫 Kata yang Mengandung Unsur Seksual
'porno', 'seks', 'mesum', 'bugil', 'telanjang', 'payudara', 'pantat', 'vagina', 'penis',
'pornoo', 'sekss', 'mesumm', 'bugill', 'telanjangg', 'payudaraa', 'pantatt', 'vaginna',
'peniss', 'pornoografi', 'pornografi', 'porno graf i', 'seksual', 'seksualitas',
'pornoograffii', 'pornografffii', 'porno graf ii', 'seksuall', 'seksualitass',
// 🤬 Kata Kasar dari Bahasa Daerah (Sunda, Jawa, dll)
'kampret', 'kacung', 'mampus', 'sialan', 'bangsat', 'goblok', 'bodoh', 'tolol',
'kamprett', 'kacungg', 'mampuss', 'sialann', 'bangsaatt', 'gobllokk', 'bodooh', 'tololl',
'kampret sunda', 'kacung jawa', 'mampus batak', 'sialan minang', 'bangsat lampung',
'goblok palembang', 'bodoh medan', 'tolol makassar',
// 📉 Kata yang Sering Digunakan untuk Menjelekkan Orang
// 'jelek', 'buruk', 'tidak pantas', 'tidak sopan', 'tidak beretika', 'tidak beradab',
// 'jelekk', 'burukk', 'tidak pantass', 'tidak sopann', 'tidak beretikaa', 'tidak beradabb',
// 'jelek banget', 'buruk banget', 'tidak pantas banget', 'tidak sopan banget',
// 'tidak beretika banget', 'tidak beradab banget',
// 🛑 Kata yang Sering Digunakan untuk Menyebarkan Hoax
// 'hoax', 'bohong', 'palsu', 'tipu', 'menipu', 'menyesatkan', 'menjerumuskan',
// 'hoaxx', 'bohongg', 'palsuu', 'tipuu', 'menipuu', 'menyesatkanng', 'menjerumuskanng',
// 'hoax besar', 'bohong besar', 'palsu besar', 'tipu besar', 'menipu besar',
// 'menyesatkan besar', 'menjerumuskan besar'
];
// Normalisasi teks
const normalizeText = (text: string) => {
return text.toLowerCase().replace(/\s+/g, ' ').trim();
};
// Cek apakah teks mengandung kata buruk
const isBadContent = (text: string) => {
const normalized = normalizeText(text);
for (let word of badWordsIndonesia) {
if (normalized.includes(word)) {
return true;
}
}
return false;
};
// Saring teks (ganti dengan asterisk)
const censorText = (text: string) => {
let result = text;
for (let word of badWordsIndonesia) {
const regex = new RegExp(word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
result = result.replace(regex, '*'.repeat(word.length));
}
return result;
};
export {
badWordsIndonesia,
isBadContent,
censorText,
normalizeText
};

18
utils/viersionBadge.ts Normal file
View File

@@ -0,0 +1,18 @@
// VersionBadge.tsx
import Constants from "expo-constants";
import { Platform } from "react-native";
export default function versionBadge() {
const expoConfig = Constants.expoConfig;
const version = expoConfig?.version; // "1.0.1"
const iosBuild = expoConfig?.ios?.buildNumber; // "10"
const androidBuild = expoConfig?.android?.versionCode; // 2
const build =
Platform.OS === "ios" ? iosBuild : androidBuild;
const result = `${version} ( ${build} )`;
return result
}