Compare commits

..

12 Commits

Author SHA1 Message Date
ccdd7730b2 Investment
Add:
- utils/pickFile: pilih extention file sesuai kebutuhan
- utils/formatCurrencyDisplay.ts: tampillan uang 2.500
- api-client/api-investment.ts
- api-storage.ts: api strogre wibudev

Fix:
- Integrasi API pada: Create, Edit, Tampilan status & detail
- Button status dan hapus data juga sudah terintegrasi

### No Issue
2025-09-29 17:42:25 +08:00
a474aebb94 Forum:
Fix: link dari avatar ke forumku & penambahan tombol create di forumku jika id forum milik user yang sama seperti user login

### No issue
2025-09-29 10:28:53 +08:00
29b65aeebf Forum
Fix:
- Integrasi API ke semua tampilan

### No Issue
2025-09-26 17:43:50 +08:00
18beb09b42 Forum
Fix:
- Tampilan beranda forum & bisa melakukan search dan sudah terintegrasi API
- Fitur hapus edit dan ubah status sudah terintegrasi API
- List komentar sudah bisa muncul dan bisa mengahpus

### No Issue
2025-09-26 14:46:29 +08:00
54af104f8a Forum
Add:
- api-client/api-forum

Fix:
- Integrasi API: create dan beranda file

### No Issue
2025-09-24 17:30:06 +08:00
8c5602b809 Collaboration
Add:
- Collaboration/GroupChatSection.tsx : fitur room chat

Fix:
- Clear code: Hapus console pada beberapa file

### No Issue
2025-09-24 15:28:16 +08:00
99f058a92f Collaboration
Add:
- (user)/collaboration/[id]/select-of-participants

Fix:
- Integrasi ke api di bagian beranda , partisipan dan group

### No Issue
2025-09-23 17:41:03 +08:00
821a211f58 Collaboration
Fix:
- Integrasi API: Beranda, create, list partisipan, check sudah berpartisipasi ?

### No Issue
2025-09-22 17:31:40 +08:00
333b1d2512 Voting
Fix: Semua tampilan sudah terintegrasi API

### No Issue
2025-09-19 17:51:08 +08:00
391430de46 Voting
Fix:
- Integrasi API pada (tabs) status & detail
- Integrasi API beranda & detail
- Integrasi API pada voting

### No Issue
2025-09-18 17:35:18 +08:00
ce79d7c240 Voting
Add:
- api-client/api-voting: kumpulan fetching api voting

Fix:
- UI create dan (tabs) status udah terintegrasi ke API

### No Isuue
2025-09-17 17:31:44 +08:00
d09a566903 Job
Add:
- add file: (user)/job/[id]/archive

Fix:
- Semua tampilan telah terintergrasi ke API Job

### No Issue
2025-09-17 14:26:10 +08:00
87 changed files with 5894 additions and 1396 deletions

View File

@@ -26,6 +26,7 @@ export default {
}, },
edgeToEdgeEnabled: true, edgeToEdgeEnabled: true,
package: 'com.bip.hipmimobileapp', package: 'com.bip.hipmimobileapp',
// softwareKeyboardLayoutMode: 'resize', // option: untuk mengatur keyboard pada room chst collaboration
}, },
web: { web: {
@@ -68,5 +69,6 @@ export default {
}, },
// Tambahkan environment variables ke sini // Tambahkan environment variables ke sini
API_BASE_URL: process.env.API_BASE_URL, API_BASE_URL: process.env.API_BASE_URL,
BASE_URL: process.env.BASE_URL,
}, },
}; };

View File

@@ -118,14 +118,13 @@ export default function UserLayout() {
headerLeft: () => <BackButton />, headerLeft: () => <BackButton />,
}} }}
/> />
<Stack.Screen {/* <Stack.Screen
name="collaboration/[id]/detail-participant" name="collaboration/[id]/detail-participant"
options={{ options={{
title: "Partisipasi Proyek", title: "Partisipasi Proyek",
headerLeft: () => <BackButton />, headerLeft: () => <BackButton />,
}} }}
/> /> */}
<Stack.Screen <Stack.Screen
name="collaboration/[id]/edit" name="collaboration/[id]/edit"
options={{ options={{
@@ -133,6 +132,20 @@ export default function UserLayout() {
headerLeft: () => <BackButton />, headerLeft: () => <BackButton />,
}} }}
/> />
<Stack.Screen
name="collaboration/[id]/create-pacticipants"
options={{
title: "Ajukan Partisipasi",
headerLeft: () => <BackButton />,
}}
/>
<Stack.Screen
name="collaboration/[id]/select-of-participants"
options={{
title: "Pilih Partisipan",
headerLeft: () => <BackButton />,
}}
/>
{/* ========== End Collaboration Section ========= */} {/* ========== End Collaboration Section ========= */}
@@ -509,6 +522,13 @@ export default function UserLayout() {
headerLeft: () => <BackButton />, headerLeft: () => <BackButton />,
}} }}
/> />
<Stack.Screen
name="job/[id]/archive"
options={{
title: "Arsip Job",
headerLeft: () => <BackButton />,
}}
/>
{/* ========== End Job Section ========= */} {/* ========== End Job Section ========= */}

View File

@@ -1,38 +1,96 @@
import { BaseBox, Grid, TextCustom } from "@/components"; /* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react-hooks/exhaustive-deps */
import {
BaseBox,
Grid,
LoaderCustom,
StackCustom,
TextCustom,
} from "@/components";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper"; import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import { apiCollaborationGetAll } from "@/service/api-client/api-collaboration";
import { Feather } from "@expo/vector-icons"; import { Feather } from "@expo/vector-icons";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
import { useState, useCallback } from "react";
export default function CollaborationGroup() { export default function CollaborationGroup() {
const { user } = useAuth();
const [listData, setListData] = useState<any[]>();
const [loadingGetData, setLoadingGetData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [user?.id])
);
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiCollaborationGetAll({
category: "group",
authorId: user?.id,
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
return ( return (
<ViewWrapper hideFooter> <ViewWrapper hideFooter>
{Array.from({ length: 10 }).map((_, index) => ( {loadingGetData ? (
<BaseBox <LoaderCustom />
key={index} ) : _.isEmpty(listData) ? (
paddingBlock={5} <TextCustom align="center">Tidak ada data</TextCustom>
href={`/collaboration/${index}/${generateProjectName()}/room-chat`} ) : (
> <StackCustom>
<Grid> {listData?.map((item: any, index: any) => (
<Grid.Col span={10}> <BaseBox
<TextCustom bold>{generateProjectName()}</TextCustom> key={index}
<TextCustom size="small">2 Anggota</TextCustom> paddingBlock={5}
</Grid.Col> href={`/collaboration/${item?.ProjectCollaboration_RoomChat?.id}/${item?.ProjectCollaboration_RoomChat?.name}/room-chat`}
<Grid.Col
span={2}
style={{ alignItems: "flex-end", justifyContent: "center" }}
> >
<Feather name="chevron-right" size={20} color={MainColor.white} /> <Grid>
</Grid.Col> <Grid.Col span={10}>
</Grid> <TextCustom bold>
</BaseBox> {item?.ProjectCollaboration_RoomChat?.name}
))} </TextCustom>
<TextCustom size="small">
{
item?.ProjectCollaboration_RoomChat
?.ProjectCollaboration_AnggotaRoomChat?.length
}{" "}
Anggota
</TextCustom>
</Grid.Col>
<Grid.Col
span={2}
style={{ alignItems: "flex-end", justifyContent: "center" }}
>
<Feather
name="chevron-right"
size={20}
color={MainColor.white}
/>
</Grid.Col>
</Grid>
</BaseBox>
))}
</StackCustom>
)}
</ViewWrapper> </ViewWrapper>
); );
} }
function generateProjectName() { function generateProjectName() {
const adjectives = [ const adjectives = [
"Blue", "Blue",
@@ -65,4 +123,4 @@ function generateProjectName() {
const randomNoun = nouns[Math.floor(Math.random() * nouns.length)]; const randomNoun = nouns[Math.floor(Math.random() * nouns.length)];
return randomAdjective + randomNoun; return randomAdjective + randomNoun;
} }

View File

@@ -1,8 +1,40 @@
import { FloatingButton, ViewWrapper } from "@/components"; import {
FloatingButton,
LoaderCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import Collaboration_BoxPublishSection from "@/screens/Collaboration/BoxPublishSection"; import Collaboration_BoxPublishSection from "@/screens/Collaboration/BoxPublishSection";
import { router } from "expo-router"; import { apiCollaborationGetAll } from "@/service/api-client/api-collaboration";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function CollaborationBeranda() { export default function CollaborationBeranda() {
const [listData, setListData] = useState<any[]>();
const [loadingGetData, setLoadingGetData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [])
);
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiCollaborationGetAll({
category: "beranda",
});
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
return ( return (
<> <>
<ViewWrapper <ViewWrapper
@@ -15,13 +47,19 @@ export default function CollaborationBeranda() {
/> />
} }
> >
{Array.from({ length: 10 }).map((_, index) => ( {loadingGetData ? (
<Collaboration_BoxPublishSection <LoaderCustom />
key={index} ) : _.isEmpty(listData) ? (
id={index.toString()} <TextCustom align="center">Tidak ada data</TextCustom>
href={`/collaboration/${index}`} ) : (
/> listData?.map((item: any, index: number) => (
))} <Collaboration_BoxPublishSection
key={index}
href={`/collaboration/${item.id}`}
data={item}
/>
))
)}
</ViewWrapper> </ViewWrapper>
</> </>
); );

View File

@@ -1,15 +1,46 @@
import { ButtonCustom, Spacing } from "@/components"; /* eslint-disable react-hooks/exhaustive-deps */
import { ButtonCustom, LoaderCustom, Spacing, TextCustom } from "@/components";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper"; import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import { AccentColor, MainColor } from "@/constants/color-palet"; import { AccentColor, MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import Collaboration_BoxPublishSection from "@/screens/Collaboration/BoxPublishSection"; import Collaboration_BoxPublishSection from "@/screens/Collaboration/BoxPublishSection";
import { useState } from "react"; import { apiCollaborationGetAll } from "@/service/api-client/api-collaboration";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
import { View } from "react-native"; import { View } from "react-native";
export default function CollaborationParticipans() { export default function CollaborationParticipans() {
const [activeCategory, setActiveCategory] = useState<string | null>( const [activeCategory, setActiveCategory] = useState<
"participant" "participant" | "my-project"
>("participant");
const { user } = useAuth();
const [listData, setListData] = useState<any[]>();
const [loadingGetData, setLoadingGetData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [activeCategory])
); );
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiCollaborationGetAll({
category:
activeCategory === "participant" ? "participant" : "my-project",
authorId: user?.id,
});
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
const handlePress = (item: any) => { const handlePress = (item: any) => {
setActiveCategory(item); setActiveCategory(item);
// tambahkan logika lain seperti filter dsb. // tambahkan logika lain seperti filter dsb.
@@ -41,13 +72,13 @@ export default function CollaborationParticipans() {
<Spacing width={"2%"} /> <Spacing width={"2%"} />
<ButtonCustom <ButtonCustom
backgroundColor={ backgroundColor={
activeCategory === "main" ? MainColor.yellow : AccentColor.blue activeCategory === "my-project" ? MainColor.yellow : AccentColor.blue
} }
textColor={ textColor={
activeCategory === "main" ? MainColor.black : MainColor.white activeCategory === "my-project" ? MainColor.black : MainColor.white
} }
style={{ width: "49%" }} style={{ width: "49%" }}
onPress={() => handlePress("main")} onPress={() => handlePress("my-project")}
> >
Proyek Saya Proyek Saya
</ButtonCustom> </ButtonCustom>
@@ -56,22 +87,27 @@ export default function CollaborationParticipans() {
return ( return (
<ViewWrapper hideFooter headerComponent={headerComponent}> <ViewWrapper hideFooter headerComponent={headerComponent}>
{Array.from({ length: 10 }).map((_, index) => ( {loadingGetData ? (
<Collaboration_BoxPublishSection <LoaderCustom />
key={index.toString()} ) : _.isEmpty(listData) ? (
id={index.toString()} <TextCustom align="center">Tidak ada data</TextCustom>
username={` ${ ) : activeCategory === "participant" ? (
activeCategory === "participant" listData?.map((item: any, index: number) => (
? "Partisipasi Proyek" <Collaboration_BoxPublishSection
: "Proyek Saya" key={index.toString()}
}`} data={item?.ProjectCollaboration}
href={ href={`/collaboration/${item?.ProjectCollaboration?.id}/detail-participant`}
activeCategory === "participant" />
? `/collaboration/${index}/detail-participant` ))
: `/collaboration/${index}/detail-project-main` ) : (
} listData?.map((item: any, index: number) => (
/> <Collaboration_BoxPublishSection
))} key={index.toString()}
data={item}
href={`/collaboration/${item?.id}/detail-project-main`}
/>
))
)}
</ViewWrapper> </ViewWrapper>
); );
} }

View File

@@ -1,17 +1,41 @@
/* eslint-disable react-hooks/exhaustive-deps */
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-unused-vars */
import { import {
AvatarUsernameAndOtherComponent, AvatarUsernameAndOtherComponent,
BackButton, BackButton,
BaseBox,
BoxWithHeaderSection, BoxWithHeaderSection,
Grid, Grid,
StackCustom, StackCustom,
TextCustom, TextCustom,
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import { Stack, useLocalSearchParams } from "expo-router"; import { apiCollaborationGroup } from "@/service/api-client/api-collaboration";
import { Stack, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useState, useCallback } from "react";
export default function CollaborationRoomInfo() { export default function CollaborationRoomInfo() {
const { id, detail } = useLocalSearchParams(); const { id, detail } = useLocalSearchParams();
const [data, setData] = useState<any>();
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
const onLoadData = async () => {
try {
const response = await apiCollaborationGroup({ id: id as string });
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
return ( return (
<> <>
<Stack.Screen <Stack.Screen
@@ -24,7 +48,7 @@ export default function CollaborationRoomInfo() {
<ViewWrapper> <ViewWrapper>
<BoxWithHeaderSection> <BoxWithHeaderSection>
<StackCustom> <StackCustom>
{listData.map((item, index) => ( {listData({ data }).map((item, index) => (
<Grid key={index}> <Grid key={index}>
<Grid.Col span={4}> <Grid.Col span={4}>
<TextCustom bold>{item.title}</TextCustom> <TextCustom bold>{item.title}</TextCustom>
@@ -37,37 +61,42 @@ export default function CollaborationRoomInfo() {
</StackCustom> </StackCustom>
</BoxWithHeaderSection> </BoxWithHeaderSection>
<BoxWithHeaderSection> <BaseBox>
{Array.from({ length: 10 }).map((_, index) => ( <StackCustom gap={10}>
<AvatarUsernameAndOtherComponent key={index} avatarHref={`/profile/${index}`} /> {data?.ProjectCollaboration_AnggotaRoomChat?.map(
))} (item: any, index: number) => (
</BoxWithHeaderSection> <AvatarUsernameAndOtherComponent
key={index}
avatarHref={`/profile/${item?.User?.Profile?.id}`}
name={item?.User?.username}
avatar={item?.User?.Profile?.imageId}
/>
)
)}
</StackCustom>
</BaseBox>
</ViewWrapper> </ViewWrapper>
</> </>
); );
} }
const listData = [ const listData = ({ data }: { data: any }) => [
{ {
title: "Judul Proyek", title: "Judul Proyek",
value: "Judul Proyek", value: data?.ProjectCollaboration?.title || "-",
}, },
{ {
title: "Industri", title: "Industri",
value: "Pilihan Industri", value:
}, data?.ProjectCollaboration?.ProjectCollaborationMaster_Industri?.name ||
{ "-",
title: "Deskripsi",
value: "Deskripsi Proyek",
}, },
{ {
title: "Tujuan Proyek", title: "Tujuan Proyek",
value: value: data?.ProjectCollaboration?.purpose || "-",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
}, },
{ {
title: "Keuntungan Proyek", title: "Keuntungan Proyek",
value: value: data?.ProjectCollaboration?.benefit || "-",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
}, },
]; ];

View File

@@ -1,68 +1,13 @@
import { import { BackButton } from "@/components";
BackButton, import { MainColor } from "@/constants/color-palet";
BoxButtonOnFooter,
Grid,
TextCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import { AccentColor, MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value"; import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import ChatScreen from "@/screens/Collaboration/GroupChatSection";
import { Feather } from "@expo/vector-icons"; import { Feather } from "@expo/vector-icons";
import { router, Stack, useLocalSearchParams } from "expo-router"; import { router, Stack, useLocalSearchParams } from "expo-router";
import { StyleSheet, TouchableOpacity, View } from "react-native";
export default function CollaborationRoomChat() { export default function CollaborationRoomChat() {
const { id, detail } = useLocalSearchParams(); const { id, detail } = useLocalSearchParams();
const inputChat = () => {
return (
<>
<BoxButtonOnFooter>
{/* <View style={{flexDirection: 'row', alignItems: 'center'}}>
<TextInputCustom placeholder="Ketik pesan..." />
<TouchableOpacity
activeOpacity={0.5}
onPress={() => console.log("Send")}
style={{
backgroundColor: AccentColor.blue,
padding: 10,
borderRadius: 50,
}}
>
<Feather name="send" size={30} color={MainColor.white} />
</TouchableOpacity>
</View> */}
<Grid>
<Grid.Col span={9}>
<TextInputCustom placeholder="Ketik pesan..." />
</Grid.Col>
<Grid.Col span={1}>
<View />
</Grid.Col>
<Grid.Col span={2} style={{ alignItems: "center" }}>
<TouchableOpacity
activeOpacity={0.5}
onPress={() => console.log("Send")}
style={{
backgroundColor: AccentColor.blue,
padding: 10,
borderRadius: 50,
}}
>
<Feather
name="send"
size={30}
color={MainColor.white}
/>
</TouchableOpacity>
</Grid.Col>
</Grid>
</BoxButtonOnFooter>
</>
);
};
return ( return (
<> <>
<Stack.Screen <Stack.Screen
@@ -79,114 +24,8 @@ export default function CollaborationRoomChat() {
), ),
}} }}
/> />
<ViewWrapper footerComponent={inputChat()}>
{dummyData.map((item, index) => ( <ChatScreen id={id as string} />
<View
key={index}
style={[
styles.messageRow,
item.role === 1 ? styles.rightAlign : styles.leftAlign,
]}
>
<View
style={[
styles.bubble,
item.role === 1 ? styles.bubbleRight : styles.bubbleLeft,
]}
>
<TextCustom style={styles.sender}>{item.nama}</TextCustom>
<TextCustom style={styles.message}>{item.chat}</TextCustom>
<TextCustom style={styles.time}>
{new Date(item.time).toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</TextCustom>
</View>
</View>
))}
{/* <TextInputCustom placeholder="Ketik pesan..." />
<Spacing/> */}
</ViewWrapper>
</> </>
); );
} }
const dummyData = [
{
nama: "Dina",
role: 1,
chat: "Hai! Kamu udah lihat dokumen proyek yang baru?",
time: "2025-07-24T09:01:15Z",
},
{
nama: "Rafi",
role: 2,
chat: "Halo! Iya, aku baru aja baca. Kayaknya kita harus revisi bagian akhir deh.",
time: "2025-07-24T09:02:03Z",
},
{
nama: "Dina",
role: 1,
chat: "Setuju. Aku juga kurang sreg sama penutupnya.",
time: "2025-07-24T09:02:45Z",
},
{
nama: "Rafi",
role: 2,
chat: "Oke, aku coba edit malam ini ya. Nanti aku share ulang versinya.",
time: "2025-07-24T09:03:10Z",
},
{
nama: "Dina",
role: 1,
chat: "Siap, makasih ya. Jangan begadang!",
time: "2025-07-24T09:03:30Z",
},
];
const styles = StyleSheet.create({
container: {
paddingVertical: 10,
paddingHorizontal: 12,
},
messageRow: {
flexDirection: "row",
marginBottom: 12,
},
rightAlign: {
justifyContent: "flex-end",
},
leftAlign: {
justifyContent: "flex-start",
},
bubble: {
maxWidth: "75%",
padding: 10,
borderRadius: 12,
},
bubbleRight: {
backgroundColor: "#DCF8C6", // hijau muda
borderTopRightRadius: 0,
},
bubbleLeft: {
backgroundColor: "#F0F0F0", // abu-abu terang
borderTopLeftRadius: 0,
},
sender: {
fontSize: 12,
fontWeight: "bold",
marginBottom: 2,
color: "#555",
},
message: {
fontSize: 15,
color: "#000",
},
time: {
fontSize: 10,
color: "#888",
textAlign: "right",
marginTop: 4,
},
});

View File

@@ -0,0 +1,80 @@
import {
AlertDefaultSystem,
ButtonCustom,
TextAreaCustom,
ViewWrapper,
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { apiCollaborationCreatePartisipasi } from "@/service/api-client/api-collaboration";
import { router, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import Toast from "react-native-toast-message";
export default function CollaborationCreatePartisipans() {
const { user } = useAuth();
const { id } = useLocalSearchParams();
const [description, setDescription] = useState("");
const [isLoading, setLoading] = useState(false);
const handlerSubmitParticipans = async () => {
try {
setLoading(true);
const response = await apiCollaborationCreatePartisipasi({
id: id as string,
data: {
authorId: user?.id,
description,
},
});
if (response.success) {
Toast.show({
type: "success",
text1: "Data berhasil disimpan",
});
router.replace(`/collaboration/${id}/list-of-participants`);
} else {
Toast.show({
type: "error",
text1: "Gagal menyimpan data",
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoading(false);
}
};
return (
<ViewWrapper>
<TextAreaCustom
// label="Deskripsi"
placeholder="Masukan deskripsi diri anda .."
value={description}
onChangeText={setDescription}
required
showCount
maxLength={1000}
/>
<ButtonCustom
disabled={description.length === 0}
isLoading={isLoading}
onPress={() => {
AlertDefaultSystem({
title: "Simpan data deskripsi",
message: "Apakah anda sudah yakin ingin menyimpan data ini ?",
textLeft: "Batal",
textRight: "Simpan",
onPressRight: () => {
handlerSubmitParticipans();
},
});
}}
>
Simpan
</ButtonCustom>
</ViewWrapper>
);
}

View File

@@ -1,26 +1,54 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
AvatarUsernameAndOtherComponent, BackButton,
BaseBox, DotButton,
DrawerCustom, DrawerCustom,
StackCustom, MenuDrawerDynamicGrid,
TextCustom, ViewWrapper
ViewWrapper,
} from "@/components"; } from "@/components";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import Collaboration_BoxDetailSection from "@/screens/Collaboration/BoxDetailSection"; import Collaboration_BoxDetailSection from "@/screens/Collaboration/BoxDetailSection";
import { MaterialIcons } from "@expo/vector-icons"; import { apiCollaborationGetOne } from "@/service/api-client/api-collaboration";
import { useLocalSearchParams } from "expo-router"; import { Ionicons } from "@expo/vector-icons";
import { useState } from "react"; import { router, Stack, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
export default function CollaborationDetailParticipant() { export default function CollaborationDetailParticipant() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const [openDrawerParticipant, setOpenDrawerParticipant] = useState(false); const [openDrawerParticipant, setOpenDrawerParticipant] = useState(false);
const [data, setData] = useState<any>();
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
const onLoadData = async () => {
try {
const response = await apiCollaborationGetOne({ id: id as string });
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
return ( return (
<> <>
<Stack.Screen
options={{
title: "Detail Proyek",
headerLeft: () => <BackButton />,
headerRight: () => (
<DotButton onPress={() => setOpenDrawerParticipant(true)} />
),
}}
/>
<ViewWrapper> <ViewWrapper>
<Collaboration_BoxDetailSection id={id as string} /> <Collaboration_BoxDetailSection data={data} />
<BaseBox style={{ height: 500 }}> {/* <BaseBox style={{ height: 500 }}>
<TextCustom align="center" bold size="large"> <TextCustom align="center" bold size="large">
Partisipan Partisipan
</TextCustom> </TextCustom>
@@ -39,13 +67,33 @@ export default function CollaborationDetailParticipant() {
} }
/> />
))} ))}
</BaseBox> </BaseBox> */}
</ViewWrapper> </ViewWrapper>
<DrawerCustom <DrawerCustom
isVisible={openDrawerParticipant} isVisible={openDrawerParticipant}
closeDrawer={() => setOpenDrawerParticipant(false)} closeDrawer={() => setOpenDrawerParticipant(false)}
height={"auto"} height={"auto"}
>
<MenuDrawerDynamicGrid
data={[
{
icon: <Ionicons name="people" size={24} color="white" />,
label: "Daftar Partisipan",
path: `/collaboration/${id}/list-of-participants`,
},
]}
onPressItem={(item) => {
router.push(item.path as any);
setOpenDrawerParticipant(false);
}}
/>
</DrawerCustom>
{/* <DrawerCustom
isVisible={openDrawerParticipant}
closeDrawer={() => setOpenDrawerParticipant(false)}
height={"auto"}
> >
<StackCustom> <StackCustom>
<TextCustom bold>Deskripsi Diri</TextCustom> <TextCustom bold>Deskripsi Diri</TextCustom>
@@ -56,7 +104,7 @@ export default function CollaborationDetailParticipant() {
Temporibus iusto soluta necessitatibus. Temporibus iusto soluta necessitatibus.
</TextCustom> </TextCustom>
</StackCustom> </StackCustom>
</DrawerCustom> </DrawerCustom> */}
</> </>
); );
} }

View File

@@ -1,30 +1,65 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
AlertDefaultSystem,
BackButton, BackButton,
ButtonCustom,
DotButton, DotButton,
DrawerCustom, DrawerCustom,
LoaderCustom,
MenuDrawerDynamicGrid, MenuDrawerDynamicGrid,
Spacing, Spacing,
StackCustom, ViewWrapper
TextCustom,
ViewWrapper,
} from "@/components"; } from "@/components";
import { IconEdit } from "@/components/_Icon"; import { IconEdit } from "@/components/_Icon";
import Collaboration_BoxDetailSection from "@/screens/Collaboration/BoxDetailSection"; import Collaboration_BoxDetailSection from "@/screens/Collaboration/BoxDetailSection";
import Collaboration_MainParticipanSelectedSection from "@/screens/Collaboration/ProjectMainSelectedSection"; import {
import { router, Stack, useLocalSearchParams } from "expo-router"; apiCollaborationGetOne
import { useState } from "react"; } from "@/service/api-client/api-collaboration";
import { MaterialIcons } from "@expo/vector-icons";
import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import { useCallback, useState } from "react";
export default function CollaborationDetailProjectMain() { export default function CollaborationDetailProjectMain() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = useState(false); const [openDrawer, setOpenDrawer] = useState(false);
const [openDrawerParticipant, setOpenDrawerParticipant] = useState(false); const [data, setData] = useState<any>();
const [selected, setSelected] = useState<(string | number)[]>([]); const [loadingGetData, setLoadingGetData] = useState(false);
const handleEdit = () => { useFocusEffect(
console.log("Edit collaboration"); useCallback(() => {
router.push("/(application)/(user)/collaboration/(id)/edit"); handlerLoadData();
}, [id])
);
const handlerLoadData = async () => {
try {
setLoadingGetData(true);
await onLoadData();
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
const onLoadData = async () => {
try {
const response = await apiCollaborationGetOne({ id: id as string });
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
const handleSubmit = (item: any) => {
console.log("item :", item);
router.push(item.path);
setOpenDrawer(false);
}; };
return ( return (
@@ -37,34 +72,21 @@ export default function CollaborationDetailProjectMain() {
}} }}
/> />
<ViewWrapper> <ViewWrapper>
<Collaboration_BoxDetailSection id={id as string} /> {loadingGetData ? (
<Collaboration_MainParticipanSelectedSection <LoaderCustom />
selected={selected} ) : (
setSelected={setSelected} <>
setOpenDrawerParticipant={setOpenDrawerParticipant} <Collaboration_BoxDetailSection data={data} />
/> {/* <Collaboration_MainParticipanSelectedSection
selected={selected}
setSelected={setSelected}
setOpenDrawerParticipant={setOpenDrawerParticipant}
listData={listData as any}
/> */}
<ButtonCustom <Spacing />
onPress={() => { </>
AlertDefaultSystem({ )}
title: "Buat Grup",
message:
"Apakah anda yakin ingin membuat grup untuk proyek ini ?",
textLeft: "Tidak",
textRight: "Ya",
onPressLeft: () => {},
onPressRight: () => {
router.navigate(
"/(application)/(user)/collaboration/(tabs)/group"
);
console.log("selected :", selected);
},
});
}}
>
Buat Grup
</ButtonCustom>
<Spacing />
</ViewWrapper> </ViewWrapper>
<DrawerCustom <DrawerCustom
@@ -76,31 +98,20 @@ export default function CollaborationDetailProjectMain() {
data={[ data={[
{ {
label: "Edit", label: "Edit",
path: "/(application)/(user)/collaboration/(tabs)/group", path: `/(application)/(user)/collaboration/${id}/edit`,
icon: <IconEdit />, icon: <IconEdit />,
}, },
{
label: "Pilih Partisipan",
path: `/(application)/(user)/collaboration/${id}/select-of-participants`,
icon: <MaterialIcons name="checklist" size={24} color="white" />,
},
]} ]}
onPressItem={(item) => { onPressItem={(item: any) => {
handleEdit(); handleSubmit(item);
}} }}
/> />
</DrawerCustom> </DrawerCustom>
<DrawerCustom
isVisible={openDrawerParticipant}
closeDrawer={() => setOpenDrawerParticipant(false)}
height={"auto"}
>
<StackCustom>
<TextCustom bold>Deskripsi Diri</TextCustom>
<TextCustom>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Commodi,
itaque adipisci. Voluptas, sed quod! Ad facere labore voluptates,
neque quidem aut reprehenderit ducimus mollitia quisquam temporibus!
Temporibus iusto soluta necessitatibus.
</TextCustom>
</StackCustom>
</DrawerCustom>
</> </>
); );
} }

View File

@@ -1,53 +1,171 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
ButtonCustom, ButtonCustom,
LoaderCustom,
SelectCustom, SelectCustom,
StackCustom, StackCustom,
TextAreaCustom, TextAreaCustom,
TextInputCustom, TextInputCustom,
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import { router } from "expo-router"; import {
apiCollaborationEditData,
apiCollaborationGetOne,
} from "@/service/api-client/api-collaboration";
import { apiMasterCollaborationType } from "@/service/api-client/api-master";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
export default function CollaborationEdit() { export default function CollaborationEdit() {
const { id } = useLocalSearchParams();
console.log("id :", id);
const [data, setData] = useState<any>();
const [listMaster, setListMaster] = useState<any[]>([]);
const [loadingData, setLoadingData] = useState(false);
const [isLoading, setIsLoading] = useState(false);
useFocusEffect(
useCallback(() => {
const fetchData = async () => {
try {
setLoadingData(true);
await onLoadData();
await onLoadMaster();
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingData(false);
}
};
fetchData();
}, [id])
);
const onLoadData = async () => {
try {
const response = await apiCollaborationGetOne({ id: id as string });
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
async function onLoadMaster() {
try {
const response = await apiMasterCollaborationType();
setListMaster(response.data);
} catch (error) {
console.log("[ERROR]", error);
}
}
const handlerSubmitUpdate = async () => {
if (
!data?.title ||
!data?.lokasi ||
!data?.projectCollaborationMaster_IndustriId ||
!data?.purpose ||
!data?.benefit
) {
Toast.show({
type: "error",
text1: "Gagal",
text2: "Harap isi semua data",
});
return;
}
try {
setIsLoading(true);
const response = await apiCollaborationEditData({
id: id as string,
data: data,
});
if (response.success) {
Toast.show({
type: "success",
text1: response.message,
});
router.back();
} else {
Toast.show({
type: "error",
text1: response.message,
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
};
return ( return (
<ViewWrapper> <ViewWrapper>
<StackCustom gap={"xs"}> {loadingData ? (
<TextInputCustom label="Judul" placeholder="Masukan judul" required /> <LoaderCustom />
<TextInputCustom label="Lokasi" placeholder="Masukan lokasi" required /> ) : (
<SelectCustom <StackCustom gap={"xs"}>
label="Pilih Industri" <TextInputCustom
data={[ label="Judul"
{ label: "Industri 1", value: "industri-1" }, placeholder="Masukan judul"
{ label: "Industri 2", value: "industri-2" }, required
{ label: "Industri 3", value: "industri-3" }, value={data?.title}
]} onChangeText={(value) => setData({ ...data, title: value })}
onChange={(value) => console.log(value)} />
/> <TextInputCustom
label="Lokasi"
placeholder="Masukan lokasi"
required
value={data?.lokasi}
onChangeText={(value) => setData({ ...data, lokasi: value })}
/>
<SelectCustom
label="Pilih Industri"
data={listMaster?.map((item: any) => ({
label: item.name,
value: item.id,
}))}
value={data?.projectCollaborationMaster_IndustriId}
onChange={(value) =>
setData({ ...data, projectCollaborationMaster_IndustriId: value })
}
/>
<TextAreaCustom <TextAreaCustom
required required
label="Tujuan Proyek" label="Tujuan Proyek"
placeholder="Masukan tujuan proyek" placeholder="Masukan tujuan proyek"
showCount showCount
maxLength={1000} maxLength={1000}
/> value={data?.purpose}
onChangeText={(value) => setData({ ...data, purpose: value })}
/>
<TextAreaCustom <TextAreaCustom
required required
label="Keuntungan Proyek" label="Keuntungan Proyek"
placeholder="Masukan keuntungan proyek" placeholder="Masukan keuntungan proyek"
showCount showCount
maxLength={1000} maxLength={1000}
/> value={data?.benefit}
onChangeText={(value) => setData({ ...data, benefit: value })}
/>
<ButtonCustom <ButtonCustom
title="Update" isLoading={isLoading}
onPress={() => { title="Update"
console.log("Update proyek"); onPress={() => {
router.back(); handlerSubmitUpdate();
}} }}
/> />
</StackCustom> </StackCustom>
)}
</ViewWrapper> </ViewWrapper>
); );
} }

View File

@@ -1,22 +1,75 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
AlertDefaultSystem,
BackButton, BackButton,
ButtonCustom, ButtonCustom,
DotButton, DotButton,
DrawerCustom, DrawerCustom,
InformationBox,
LoaderCustom,
MenuDrawerDynamicGrid, MenuDrawerDynamicGrid,
TextAreaCustom,
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import { useAuth } from "@/hooks/use-auth";
import Collaboration_BoxDetailSection from "@/screens/Collaboration/BoxDetailSection"; import Collaboration_BoxDetailSection from "@/screens/Collaboration/BoxDetailSection";
import {
apiCollaborationGetOne,
apiCollaborationGetParticipants,
} from "@/service/api-client/api-collaboration";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { router, Stack, useLocalSearchParams } from "expo-router"; import {
import { useState } from "react"; router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import { useCallback, useState } from "react";
export default function CollaborationDetail() { export default function CollaborationDetail() {
const { user } = useAuth();
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const [openDrawerPartisipasi, setOpenDrawerPartisipasi] = useState(false); const [data, setData] = useState<any>();
const [openDrawerMenu, setOpenDrawerMenu] = useState(false); const [openDrawerMenu, setOpenDrawerMenu] = useState(false);
const [isParticipant, setIsParticipant] = useState(false);
const [loadingIsParticipant, setLoadingIsParticipant] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
onLoadParticipants();
}, [id])
);
const onLoadData = async () => {
try {
const response = await apiCollaborationGetOne({ id: id as string });
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
const onLoadParticipants = async () => {
try {
setLoadingIsParticipant(true);
const response = await apiCollaborationGetParticipants({
category: "check-participant",
id: id as string,
authorId: user?.id,
});
if (response.success) {
setIsParticipant(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingIsParticipant(false);
}
};
return ( return (
<> <>
<Stack.Screen <Stack.Screen
@@ -29,15 +82,35 @@ export default function CollaborationDetail() {
}} }}
/> />
<ViewWrapper> <ViewWrapper>
<Collaboration_BoxDetailSection id={id as string} /> {!data && !isParticipant ? (
<LoaderCustom />
<ButtonCustom onPress={() => setOpenDrawerPartisipasi(true)}> ) : (
Partisipasi <>
</ButtonCustom> {user?.id === data?.Author?.id && (
<InformationBox
text={
"Tombol partisipasi hanya muncul pada proyek milik orang lain"
}
/>
)}
<Collaboration_BoxDetailSection data={data} />
{user?.id !== data?.Author?.id && (
<ButtonCustom
disabled={isParticipant || loadingIsParticipant}
onPress={() => {
router.push(`/collaboration/${id}/create-pacticipants`);
// setOpenDrawerPartisipasi(true);
}}
>
{isParticipant ? "Anda telah berpartisipasi" : "Partisipasi"}
</ButtonCustom>
)}
</>
)}
</ViewWrapper> </ViewWrapper>
{/* Drawer Partisipasi */} {/* Drawer Partisipasi */}
<DrawerCustom {/* <DrawerCustom
isVisible={openDrawerPartisipasi} isVisible={openDrawerPartisipasi}
closeDrawer={() => setOpenDrawerPartisipasi(false)} closeDrawer={() => setOpenDrawerPartisipasi(false)}
height={300} height={300}
@@ -48,6 +121,8 @@ export default function CollaborationDetail() {
required required
showCount showCount
maxLength={500} maxLength={500}
value={description}
onChangeText={setDescription}
/> />
<ButtonCustom <ButtonCustom
@@ -58,19 +133,21 @@ export default function CollaborationDetail() {
message: "Apakah anda sudah yakin ingin menyimpan data ini ?", message: "Apakah anda sudah yakin ingin menyimpan data ini ?",
textLeft: "Batal", textLeft: "Batal",
textRight: "Simpan", textRight: "Simpan",
onPressRight: () => router.replace(`/collaboration/(tabs)/group`), onPressRight: () => {
handlerSubmitParticipans();
},
}); });
}} }}
> >
Simpan Simpan
</ButtonCustom> </ButtonCustom>
</DrawerCustom> </DrawerCustom> */}
{/* Drawer Menu */} {/* Drawer Menu */}
<DrawerCustom <DrawerCustom
isVisible={openDrawerMenu} isVisible={openDrawerMenu}
closeDrawer={() => setOpenDrawerMenu(false)} closeDrawer={() => setOpenDrawerMenu(false)}
height={250} height={"auto"}
> >
<MenuDrawerDynamicGrid <MenuDrawerDynamicGrid
data={[ data={[

View File

@@ -1,38 +1,79 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
AvatarUsernameAndOtherComponent, AvatarUsernameAndOtherComponent,
BaseBox, BaseBox,
DrawerCustom, DrawerCustom,
Spacing, LoaderCustom,
StackCustom, Spacing,
TextCustom, StackCustom,
ViewWrapper TextCustom,
ViewWrapper,
} from "@/components"; } from "@/components";
import { apiCollaborationGetParticipants } from "@/service/api-client/api-collaboration";
import { Feather } from "@expo/vector-icons"; import { Feather } from "@expo/vector-icons";
import { useLocalSearchParams } from "expo-router"; import { useLocalSearchParams } from "expo-router";
import { useState } from "react"; import _ from "lodash";
import { useEffect, useState } from "react";
import { ScrollView } from "react-native"; import { ScrollView } from "react-native";
export default function CollaborationListOfParticipants() { export default function CollaborationListOfParticipants() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const [listData, setListData] = useState<any[]>();
const [loadingGetData, setLoadingGetData] = useState(false);
const [description, setDescription] = useState("");
useEffect(() => {
onLoadData();
}, [id]);
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiCollaborationGetParticipants({
category: "list",
id: id as string,
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
const [openDrawer, setOpenDrawer] = useState(false); const [openDrawer, setOpenDrawer] = useState(false);
return ( return (
<> <>
<ViewWrapper> <ViewWrapper>
{Array.from({ length: 10 }).map((_, index) => ( {loadingGetData ? (
<BaseBox key={index} paddingBlock={5}> <LoaderCustom />
<AvatarUsernameAndOtherComponent ) : _.isEmpty(listData) ? (
avatarHref={`/profile/${id}`} <TextCustom align="center">Tidak ada partisipan</TextCustom>
rightComponent={ ) : (
<Feather listData?.map((item: any, index: number) => (
name="chevron-right" <BaseBox key={index} paddingBlock={5}>
size={24} <AvatarUsernameAndOtherComponent
color="white" avatar={item?.User?.Profile?.imageId}
onPress={() => setOpenDrawer(true)} avatarHref={`/profile/${item?.User?.Profile?.id}`}
/> name={item?.User?.username}
} rightComponent={
/> <Feather
</BaseBox> name="chevron-right"
))} size={24}
color="white"
onPress={() => {
setDescription(item?.deskripsi_diri);
setOpenDrawer(true);
}}
/>
}
/>
</BaseBox>
))
)}
</ViewWrapper> </ViewWrapper>
{/* Drawer */} {/* Drawer */}
@@ -44,34 +85,7 @@ export default function CollaborationListOfParticipants() {
<TextCustom bold>Deskripsi diri</TextCustom> <TextCustom bold>Deskripsi diri</TextCustom>
<BaseBox> <BaseBox>
<ScrollView style={{ height: "80%" }}> <ScrollView style={{ height: "80%" }}>
<TextCustom> <TextCustom>{description}</TextCustom>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua.
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua.Lorem
ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua.Lorem
ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua.Lorem
ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut iqua.Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua.Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua.Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua.Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua.Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua.Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua.Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua.Lorem ipsum dolor sit amet,
consectetur adipiscing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua.
</TextCustom>
</ScrollView> </ScrollView>
</BaseBox> </BaseBox>
<Spacing /> <Spacing />

View File

@@ -0,0 +1,251 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
AlertDefaultSystem,
AvatarComp,
BaseBox,
BoxButtonOnFooter,
ButtonCustom,
CheckboxCustom,
CheckboxGroup,
DrawerCustom,
Grid,
LoaderCustom,
Spacing,
StackCustom,
TextAreaCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import ModalCustom from "@/components/Modal/ModalCustom";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";
import {
apiCollaborationCreateGroup,
apiCollaborationGetParticipants,
} from "@/service/api-client/api-collaboration";
import { MaterialIcons } from "@expo/vector-icons";
import { router, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useEffect, useState } from "react";
import { ScrollView, View } from "react-native";
import Toast from "react-native-toast-message";
export default function CollaborationSelectOfParticipants() {
const { user } = useAuth();
const { id } = useLocalSearchParams();
const [listData, setListData] = useState<any[]>();
const [loadingGetData, setLoadingGetData] = useState(false);
const [description, setDescription] = useState("");
const [selected, setSelected] = useState<(string | number)[]>([]);
const [openDrawer, setOpenDrawer] = useState(false);
const [nameGroup, setNameGroup] = useState("");
const [openModal, setOpenModal] = useState(false);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
onLoadData();
}, [id]);
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiCollaborationGetParticipants({
category: "list",
id: id as string,
});
// console.log("response :", JSON.stringify(response.data, null, 2));
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
const handlerCreateGroup = async () => {
if (_.isEmpty(nameGroup)) {
Toast.show({
type: "error",
text1: "Nama grup tidak boleh kosong",
});
return;
}
try {
setIsLoading(true);
const response = await apiCollaborationCreateGroup({
id: id as string,
data: {
authorId: user?.id,
nameGroup: nameGroup,
listSelect: selected,
},
});
if (response.success) {
Toast.show({
type: "success",
text1: "Grup berhasil dibuat",
});
router.push("/(application)/(user)/collaboration/(tabs)/group");
} else {
Toast.show({
type: "error",
text1: "Gagal membuat grup",
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
};
const handlerSubmit = () => {
return (
<>
<BoxButtonOnFooter>
<ButtonCustom
disabled={_.isEmpty(selected)}
onPress={() => {
setOpenModal(true);
setNameGroup("");
}}
>
Buat Grup
</ButtonCustom>
</BoxButtonOnFooter>
</>
);
};
return (
<>
<ViewWrapper
footerComponent={_.isEmpty(listData) ? null : handlerSubmit()}
>
{loadingGetData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada partisipan</TextCustom>
) : (
<StackCustom>
<TextCustom size="default" color="red" bold>
*{" "}
<TextCustom size="small" semiBold>
Pilih user yang akan menjadi tim proyek anda
</TextCustom>
</TextCustom>
<CheckboxGroup
value={selected}
onChange={(val: any) => {
console.log("val :", val);
setSelected(val);
}}
>
{listData?.map((item: any, index: any) => (
<View key={index}>
<Grid key={index}>
<Grid.Col
span={2}
style={{ alignItems: "center", justifyContent: "center" }}
>
<CheckboxCustom valueKey={item?.User?.id} />
</Grid.Col>
<Grid.Col span={2} style={{ alignItems: "center" }}>
<AvatarComp
size="base"
fileId={item?.User?.Profile?.imageId}
/>
</Grid.Col>
<Grid.Col span={6} style={{ justifyContent: "center" }}>
<TextCustom bold truncate>
{item?.User?.username}
</TextCustom>
</Grid.Col>
<Grid.Col
span={2}
style={{ alignItems: "center", justifyContent: "center" }}
>
<MaterialIcons
name="notes"
size={ICON_SIZE_SMALL}
color="white"
onPress={() => {
setOpenDrawer(true);
setDescription(item?.deskripsi_diri);
}}
/>
</Grid.Col>
</Grid>
</View>
))}
</CheckboxGroup>
</StackCustom>
)}
</ViewWrapper>
<ModalCustom isVisible={openModal}>
<StackCustom gap={0}>
<TextAreaCustom
placeholder="Nama Grup"
value={nameGroup}
onChangeText={(val) => setNameGroup(val)}
/>
<Grid>
<Grid.Col span={6} style={{ paddingRight: 10 }}>
<ButtonCustom
backgroundColor="gray"
onPress={() => {
setOpenModal(false);
}}
>
Batal
</ButtonCustom>
</Grid.Col>
<Grid.Col span={6} style={{ paddingLeft: 10 }}>
<ButtonCustom
isLoading={isLoading}
disabled={_.isEmpty(nameGroup)}
onPress={() => {
AlertDefaultSystem({
title: "Buat Grup",
message:
"Apakah anda yakin ingin membuat grup untuk proyek ini ?",
textLeft: "Tidak",
textRight: "Ya",
onPressLeft: () => {},
onPressRight: () => {
handlerCreateGroup();
},
});
}}
>
Simpan
</ButtonCustom>
</Grid.Col>
</Grid>
</StackCustom>
</ModalCustom>
{/* Drawer */}
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
>
<StackCustom>
<TextCustom bold>Deskripsi diri</TextCustom>
<BaseBox>
<ScrollView style={{ height: "80%" }}>
<TextCustom>{description}</TextCustom>
</ScrollView>
</BaseBox>
<Spacing />
</StackCustom>
</DrawerCustom>
</>
);
}

View File

@@ -1,53 +1,174 @@
import { import {
ButtonCustom, ButtonCustom,
SelectCustom, LoaderCustom,
StackCustom, SelectCustom,
TextAreaCustom, StackCustom,
TextInputCustom, TextAreaCustom,
ViewWrapper TextInputCustom,
ViewWrapper,
} from "@/components"; } from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { apiCollaborationCreate } from "@/service/api-client/api-collaboration";
import { apiMasterCollaborationType } from "@/service/api-client/api-master";
import { router } from "expo-router"; import { router } from "expo-router";
import React, { useEffect, useState } from "react";
import Toast from "react-native-toast-message";
interface CollaborationCreateProps {
title?: string;
lokasi?: string;
purpose?: string;
benefit?: string;
projectCollaborationMaster_IndustriId?: string;
userId?: string;
}
export default function CollaborationCreate() { export default function CollaborationCreate() {
const { user } = useAuth();
const [listMaster, setListMaster] = useState<any>([]);
const [loadingMaster, setLoadingMaster] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = React.useState<CollaborationCreateProps>({
title: "",
lokasi: "",
purpose: "",
benefit: "",
projectCollaborationMaster_IndustriId: "",
userId: "",
});
useEffect(() => {
onLoadMaster();
}, []);
async function onLoadMaster() {
try {
setLoadingMaster(true);
const response = await apiMasterCollaborationType();
setListMaster(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingMaster(false);
}
}
const handlerSubmit = async () => {
if (
!data?.title ||
!data?.lokasi ||
!data?.purpose ||
!data?.benefit ||
!data?.projectCollaborationMaster_IndustriId
) {
Toast.show({
type: "error",
text1: "Gagal",
text2: "Harap isi semua data",
});
return;
}
const newData: CollaborationCreateProps = {
title: data?.title,
lokasi: data?.lokasi,
purpose: data?.purpose,
benefit: data?.benefit,
projectCollaborationMaster_IndustriId:
data?.projectCollaborationMaster_IndustriId,
userId: user?.id,
};
try {
setIsLoading(true);
const response = await apiCollaborationCreate({ data: newData });
if (response.success) {
Toast.show({
type: "success",
text1: "Berhasil",
text2: response.message,
});
router.back();
} else {
Toast.show({
type: "error",
text1: "Gagal",
text2: response.message,
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
};
return ( return (
<ViewWrapper> <ViewWrapper>
<StackCustom gap={"xs"}> {loadingMaster ? (
<TextInputCustom label="Judul" placeholder="Masukan judul" required /> <LoaderCustom />
<TextInputCustom label="Lokasi" placeholder="Masukan lokasi" required /> ) : (
<SelectCustom <StackCustom gap={"xs"}>
label="Pilih Industri" <TextInputCustom
data={[ label="Judul"
{ label: "Industri 1", value: "industri-1" }, placeholder="Masukan judul"
{ label: "Industri 2", value: "industri-2" }, required
{ label: "Industri 3", value: "industri-3" }, value={data?.title}
]} onChangeText={(value: any) => setData({ ...data, title: value })}
onChange={(value) => console.log(value)} />
/>
<TextAreaCustom <TextInputCustom
required label="Lokasi"
label="Tujuan Proyek" placeholder="Masukan lokasi"
placeholder="Masukan tujuan proyek" required
showCount value={data?.lokasi}
maxLength={1000} onChangeText={(value: any) => setData({ ...data, lokasi: value })}
/> />
<TextAreaCustom <SelectCustom
required label="Pilih Industri"
label="Keuntungan Proyek" data={listMaster?.map((item: any) => ({
placeholder="Masukan keuntungan proyek" label: item.name,
showCount value: item.id,
maxLength={1000} }))}
/> value={data?.projectCollaborationMaster_IndustriId}
onChange={(value: any) => {
console.log(value);
setData({
...data,
projectCollaborationMaster_IndustriId: value,
});
}}
/>
<ButtonCustom <TextAreaCustom
title="Simpan" required
onPress={() => { label="Tujuan Proyek"
console.log("Simpan proyek"); placeholder="Masukan tujuan proyek"
router.back(); showCount
}} maxLength={1000}
/> value={data?.purpose}
</StackCustom> onChangeText={(value: any) => setData({ ...data, purpose: value })}
/>
<TextAreaCustom
required
label="Keuntungan Proyek"
placeholder="Masukan keuntungan proyek"
showCount
maxLength={1000}
value={data?.benefit}
onChangeText={(value: any) => setData({ ...data, benefit: value })}
/>
<ButtonCustom
isLoading={isLoading}
title="Simpan"
onPress={() => handlerSubmit()}
/>
</StackCustom>
)}
</ViewWrapper> </ViewWrapper>
); );
} }

View File

@@ -1,37 +1,98 @@
import { import {
BoxButtonOnFooter, BoxButtonOnFooter,
ButtonCustom, ButtonCustom,
LoaderCustom,
TextAreaCustom, TextAreaCustom,
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import { router } from "expo-router"; import { apiForumGetOne, apiForumUpdate } from "@/service/api-client/api-forum";
import { useState } from "react"; import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
export default function ForumEdit() { export default function ForumEdit() {
const { id } = useLocalSearchParams();
const [text, setText] = useState(""); const [text, setText] = useState("");
const [loadingGetData, setLoadingGetData] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const buttonFooter = ( useFocusEffect(
<BoxButtonOnFooter> useCallback(() => {
<ButtonCustom onLoadData(id as string);
onPress={() => { }, [id])
console.log("Posting", text);
router.back();
}}
>
Update
</ButtonCustom>
</BoxButtonOnFooter>
); );
const onLoadData = async (id: string) => {
try {
setLoadingGetData(true);
const response = await apiForumGetOne({ id });
setText(response.data.diskusi);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
const handlerUpdateData = async () => {
if (!text) {
Toast.show({
type: "error",
text1: "Harap masukkan diskusi",
});
return;
}
try {
setIsLoading(true);
const response = await apiForumUpdate({
id: id as string,
data: text,
});
if (response.success) {
Toast.show({
type: "success",
text1: "Berhasil diupdate",
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
};
const buttonFooter = () => {
return (
<>
{!loadingGetData && (
<BoxButtonOnFooter>
<ButtonCustom isLoading={isLoading} onPress={handlerUpdateData}>
Update
</ButtonCustom>
</BoxButtonOnFooter>
)}
</>
);
};
return ( return (
<ViewWrapper footerComponent={buttonFooter}> <ViewWrapper footerComponent={buttonFooter()}>
<TextAreaCustom {!loadingGetData ? (
placeholder="Ketik diskusi anda..." <TextAreaCustom
maxLength={1000} placeholder="Ketik diskusi anda..."
showCount maxLength={1000}
value={text} showCount
onChangeText={setText} value={text}
/> onChangeText={(value) => {
setText(value);
}}
/>
) : (
<LoaderCustom />
)}
</ViewWrapper> </ViewWrapper>
); );
} }

View File

@@ -1,35 +1,86 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
AlertCustom, AvatarComp,
AvatarCustom,
ButtonCustom, ButtonCustom,
CenterCustom, CenterCustom,
DrawerCustom, DrawerCustom,
FloatingButton,
Grid, Grid,
LoaderCustom,
StackCustom, StackCustom,
TextCustom, TextCustom,
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import { MainColor } from "@/constants/color-palet"; import { useAuth } from "@/hooks/use-auth";
import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection"; import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection";
import { listDummyDiscussionForum } from "@/screens/Forum/list-data-dummy";
import Forum_MenuDrawerBerandaSection from "@/screens/Forum/MenuDrawerSection.tsx/MenuBeranda"; import Forum_MenuDrawerBerandaSection from "@/screens/Forum/MenuDrawerSection.tsx/MenuBeranda";
import { useLocalSearchParams } from "expo-router"; import { apiForumGetAll } from "@/service/api-client/api-forum";
import { useState } from "react"; 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 Forumku() { export default function Forumku() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const { user } = useAuth();
const [openDrawer, setOpenDrawer] = useState(false); const [openDrawer, setOpenDrawer] = useState(false);
const [status, setStatus] = useState(""); const [status, setStatus] = useState("");
const [alertStatus, setAlertStatus] = useState(false); const [listData, setListData] = useState<any | null>(null);
const [deleteAlert, setDeleteAlert] = useState(false); 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 ( return (
<> <>
<ViewWrapper> <ViewWrapper
floatingButton={
user?.id === id && (
<FloatingButton
onPress={() =>
router.navigate("/(application)/(user)/forum/create")
}
/>
)
}
>
<StackCustom> <StackCustom>
<CenterCustom> <CenterCustom>
<AvatarCustom <AvatarComp
href={`/(application)/(image)/preview-image/${id}`} fileId={dataUser?.Profile?.imageId}
href={`/(application)/(image)/preview-image/${dataUser?.Profile?.imageId}`}
size="xl" size="xl"
/> />
</CenterCustom> </CenterCustom>
@@ -37,32 +88,43 @@ export default function Forumku() {
<Grid> <Grid>
<Grid.Col span={6}> <Grid.Col span={6}>
<TextCustom bold truncate> <TextCustom bold truncate>
@bagas_banuna @{dataUser?.username || "-"}
</TextCustom> </TextCustom>
<TextCustom>1 postingan</TextCustom> <TextCustom>{listData?.length || "0"} postingan</TextCustom>
</Grid.Col> </Grid.Col>
<Grid.Col span={6} style={{ alignItems: "flex-end" }}> <Grid.Col span={6} style={{ alignItems: "flex-end" }}>
<ButtonCustom href={`/profile/${id}`}> <ButtonCustom href={`/profile/${dataUser?.Profile?.id}`}>
Kunjungi Profile Kunjungi Profile
</ButtonCustom> </ButtonCustom>
</Grid.Col> </Grid.Col>
</Grid> </Grid>
{listDummyDiscussionForum.map((e, i) => ( {loadingGetList ? (
<Forum_BoxDetailSection <LoaderCustom />
key={i} ) : _.isEmpty(listData) ? (
data={e} <TextCustom> Tidak ada diskusi</TextCustom>
setOpenDrawer={setOpenDrawer} ) : (
setStatus={setStatus} <>
isTruncate={true} {listData?.map((item: any, index: number) => (
href={`/forum/${id}`} <Forum_BoxDetailSection
/> isRightComponent={false}
))} key={index}
data={item}
isTruncate={true}
href={`/forum/${item.id}`}
onSetData={(value) => {
setOpenDrawer(value.setOpenDrawer);
setStatus(value.setStatus);
}}
/>
))}
</>
)}
</StackCustom> </StackCustom>
</ViewWrapper> </ViewWrapper>
{/* Drawer Komponen Eksternal */} {/* Drawer Komponen Eksternal */}
<DrawerCustom <DrawerCustom
height={350} height={"auto"}
isVisible={openDrawer} isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)} closeDrawer={() => setOpenDrawer(false)}
> >
@@ -72,42 +134,9 @@ export default function Forumku() {
setIsDrawerOpen={() => { setIsDrawerOpen={() => {
setOpenDrawer(false); setOpenDrawer(false);
}} }}
setShowDeleteAlert={setDeleteAlert} authorId={id as string}
setShowAlertStatus={setAlertStatus}
/> />
</DrawerCustom> </DrawerCustom>
{/* Alert Komponen Eksternal */}
<AlertCustom
isVisible={alertStatus}
onLeftPress={() => setAlertStatus(false)}
onRightPress={() => {
setOpenDrawer(false);
setAlertStatus(false);
console.log("Ubah status forum");
}}
title="Ubah Status Forum"
message="Apakah Anda yakin ingin mengubah status forum ini?"
textLeft="Batal"
textRight="Ubah"
colorRight={MainColor.green}
/>
{/* Alert Delete */}
<AlertCustom
isVisible={deleteAlert}
onLeftPress={() => setDeleteAlert(false)}
onRightPress={() => {
setOpenDrawer(false);
setDeleteAlert(false);
console.log("Hapus forum");
}}
title="Hapus Forum"
message="Apakah Anda yakin ingin menghapus forum ini?"
textLeft="Batal"
textRight="Hapus"
colorRight={MainColor.red}
/>
</> </>
); );
} }

View File

@@ -1,176 +1,263 @@
import { import {
AlertCustom,
ButtonCustom, ButtonCustom,
DrawerCustom, DrawerCustom,
LoaderCustom,
Spacing, Spacing,
TextAreaCustom, TextAreaCustom,
TextCustom,
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import { MainColor } from "@/constants/color-palet"; import { useAuth } from "@/hooks/use-auth";
import Forum_CommentarBoxSection from "@/screens/Forum/CommentarBoxSection"; import Forum_CommentarBoxSection from "@/screens/Forum/CommentarBoxSection";
import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection"; import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection";
import { listDummyCommentarForum } from "@/screens/Forum/list-data-dummy";
import Forum_MenuDrawerBerandaSection from "@/screens/Forum/MenuDrawerSection.tsx/MenuBeranda"; import Forum_MenuDrawerBerandaSection from "@/screens/Forum/MenuDrawerSection.tsx/MenuBeranda";
import Forum_MenuDrawerCommentar from "@/screens/Forum/MenuDrawerSection.tsx/MenuCommentar"; import Forum_MenuDrawerCommentar from "@/screens/Forum/MenuDrawerSection.tsx/MenuCommentar";
import { router, useLocalSearchParams } from "expo-router"; import {
import { useState } from "react"; apiForumCreateComment,
apiForumGetComment,
apiForumGetOne,
apiForumUpdateStatus,
} from "@/service/api-client/api-forum";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useEffect, useState } from "react";
interface CommentProps {
id: string;
isActive: boolean;
komentar: string;
createdAt: Date;
authorId: string;
Author: {
id: string;
username: string;
Profile: {
id: string;
imageId: string;
};
};
}
export default function ForumDetail() { export default function ForumDetail() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
console.log(id); const { user } = useAuth();
const [openDrawer, setOpenDrawer] = useState(false); const [openDrawer, setOpenDrawer] = useState(false);
const [data, setData] = useState<any | null>(null);
const [listComment, setListComment] = useState<CommentProps[] | null>(null);
const [isLoadingComment, setLoadingComment] = useState(false);
// Status
const [status, setStatus] = useState(""); const [status, setStatus] = useState("");
const [alertStatus, setAlertStatus] = useState(false);
const [deleteAlert, setDeleteAlert] = useState(false);
const [text, setText] = useState(""); const [text, setText] = useState("");
const [authorId, setAuthorId] = useState("");
const [dataId, setDataId] = useState("");
// Comentar // Comentar
const [openDrawerCommentar, setOpenDrawerCommentar] = useState(false); const [openDrawerCommentar, setOpenDrawerCommentar] = useState(false);
const [alertDeleteCommentar, setAlertDeleteCommentar] = useState(false); const [commentId, setCommentId] = useState("");
const [commentAuthorId, setCommentAuthorId] = useState("");
const dataDummy = { useFocusEffect(
name: "Bagas", useCallback(() => {
status: "Open", onLoadData(id as string);
date: "14/07/2025", }, [id])
deskripsi: );
"Lorem ipsum dolor sit amet consectetur adipisicing elit. Vitae inventore iure pariatur, libero omnis excepturi. Ullam ad officiis deleniti quos esse odit nesciunt, ipsam adipisci cumque aliquam corporis culpa fugit?",
jumlahBalas: 2, const onLoadData = async (id: string) => {
try {
const response = await apiForumGetOne({ id });
setData(response.data);
} catch (error) {
console.log("[ERROR]", error);
}
};
useEffect(() => {
onLoadListComment(id as string);
}, [id]);
const onLoadListComment = async (id: string) => {
try {
const response = await apiForumGetComment({
id: id as string,
});
setListComment(response.data);
} catch (error) {
console.log("[ERROR]", error);
}
};
// Update Status
const handlerUpdateStatus = async (value: any) => {
try {
const response = await apiForumUpdateStatus({
id: id as string,
data: value,
});
if (response.success) {
setStatus(response.data);
setData({
...data,
ForumMaster_StatusPosting: {
status: response.data,
},
});
}
} catch (error) {
console.log("[ERROR]", error);
}
};
// Create Commentar
const handlerCreateCommentar = async () => {
const newData = {
comment: text,
authorId: user?.id,
};
try {
setLoadingComment(true);
const response = await apiForumCreateComment({
id: id as string,
data: newData,
});
if (response.success) {
setText("");
const newComment = {
id: response.data.id,
isActive: response.data.isActive,
komentar: response.data.komentar,
createdAt: response.data.createdAt,
authorId: response.data.authorId,
Author: response.data.Author,
};
setListComment((prev) => [newComment, ...(prev || [])]);
setData({
...data,
count: data.count + 1,
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingComment(false);
}
}; };
return ( return (
<> <>
<ViewWrapper> <ViewWrapper>
{/* <StackCustom> {!data && !listComment ? (
</StackCustom> */} <LoaderCustom />
<Forum_BoxDetailSection ) : (
data={dataDummy} <>
setOpenDrawer={setOpenDrawer} {/* Box Posting */}
setStatus={setStatus} <Forum_BoxDetailSection
/> data={data}
onSetData={() => {
setOpenDrawer(true);
setStatus(data.ForumMaster_StatusPosting?.status);
setAuthorId(data.Author?.id);
setDataId(data.id);
}}
/>
<TextAreaCustom {/* Area Commentar */}
placeholder="Ketik diskusi anda..." {data?.ForumMaster_StatusPosting?.status === "Open" && (
maxLength={1000} <>
showCount <TextAreaCustom
value={text} placeholder="Ketik diskusi anda..."
onChangeText={setText} maxLength={1000}
style={{ showCount
marginBottom: 0, value={text}
}} onChangeText={setText}
/> style={{
<ButtonCustom marginBottom: 0,
style={{ }}
alignSelf: "flex-end", />
}} <ButtonCustom
onPress={() => { isLoading={isLoadingComment}
console.log("Posting", text); style={{
router.back(); alignSelf: "flex-end",
}} }}
> onPress={() => {
Balas handlerCreateCommentar();
</ButtonCustom> }}
>
Balas
</ButtonCustom>
</>
)}
<Spacing height={40} />
<Spacing height={40} /> {/* List Commentar */}
{_.isEmpty(listComment) ? (
{listDummyCommentarForum.map((e, i) => ( <TextCustom align="center" color="gray" size={"small"}>
<Forum_CommentarBoxSection Tidak ada komentar
key={i} </TextCustom>
data={e} ) : (
setOpenDrawer={setOpenDrawerCommentar} <TextCustom color="gray">Komentar :</TextCustom>
/> )}
))} <Spacing height={5} />
{listComment?.map((item: any, index: number) => (
<Forum_CommentarBoxSection
key={index}
data={item}
onSetData={(value) => {
setCommentId(value.setCommentId);
setOpenDrawerCommentar(value.setOpenDrawer);
setCommentAuthorId(value.setCommentAuthorId);
}}
/>
))}
</>
)}
</ViewWrapper> </ViewWrapper>
{/* Posting Drawer */}
<DrawerCustom <DrawerCustom
height={350} height={"auto"}
isVisible={openDrawer} isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)} closeDrawer={() => setOpenDrawer(false)}
> >
<Forum_MenuDrawerBerandaSection <Forum_MenuDrawerBerandaSection
id={id as string} id={dataId}
status={status} status={status}
setIsDrawerOpen={() => { setIsDrawerOpen={() => {
setOpenDrawer(false); setOpenDrawer(false);
}} }}
setShowDeleteAlert={setDeleteAlert} authorId={authorId}
setShowAlertStatus={setAlertStatus} handlerUpdateStatus={(value: any) => {
handlerUpdateStatus(value);
}}
/> />
</DrawerCustom> </DrawerCustom>
{/* Alert Status */} {/* Commentar Drawer */}
<AlertCustom
isVisible={alertStatus}
title="Ubah Status Forum"
message="Apakah Anda yakin ingin mengubah status forum ini?"
onLeftPress={() => {
setOpenDrawer(false);
setAlertStatus(false);
console.log("Batal");
}}
onRightPress={() => {
setOpenDrawer(false);
setAlertStatus(false);
console.log("Ubah status forum");
}}
textLeft="Batal"
textRight="Ubah"
colorRight={MainColor.green}
/>
{/* Alert Delete */}
<AlertCustom
isVisible={deleteAlert}
title="Hapus Forum"
message="Apakah Anda yakin ingin menghapus forum ini?"
onLeftPress={() => {
setOpenDrawer(false);
setDeleteAlert(false);
console.log("Batal");
}}
onRightPress={() => {
setOpenDrawer(false);
setDeleteAlert(false);
console.log("Hapus forum");
}}
textLeft="Batal"
textRight="Hapus"
colorRight={MainColor.red}
/>
{/* Commentar */}
<DrawerCustom <DrawerCustom
height={350} height={"auto"}
isVisible={openDrawerCommentar} isVisible={openDrawerCommentar}
closeDrawer={() => setOpenDrawerCommentar(false)} closeDrawer={() => setOpenDrawerCommentar(false)}
> >
<Forum_MenuDrawerCommentar <Forum_MenuDrawerCommentar
id={id as string} id={commentId as string}
commentId={commentId}
commentAuthorId={commentAuthorId}
setIsDrawerOpen={() => { setIsDrawerOpen={() => {
setOpenDrawerCommentar(false); setOpenDrawerCommentar(false);
}} }}
setShowDeleteAlert={setAlertDeleteCommentar} listComment={listComment}
setListComment={setListComment}
countComment={data?.count}
setCountComment={(val: any) => {
setData((prev: any) => ({
...prev,
count: val,
}));
}}
/> />
</DrawerCustom> </DrawerCustom>
{/* Alert Delete Commentar */}
<AlertCustom
isVisible={alertDeleteCommentar}
title="Hapus Komentar"
message="Apakah Anda yakin ingin menghapus komentar ini?"
onLeftPress={() => {
setOpenDrawerCommentar(false);
setAlertDeleteCommentar(false);
console.log("Batal");
}}
onRightPress={() => {
setOpenDrawerCommentar(false);
setAlertDeleteCommentar(false);
console.log("Hapus commentar");
}}
textLeft="Batal"
textRight="Hapus"
colorRight={MainColor.red}
/>
</> </>
); );
} }

View File

@@ -5,27 +5,69 @@ import {
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { router } from "expo-router"; import { useAuth } from "@/hooks/use-auth";
import { apiForumCreateReportCommentar } from "@/service/api-client/api-master";
import { router, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import Toast from "react-native-toast-message";
export default function ForumOtherReportCommentar() { export default function ForumOtherReportCommentar() {
const { id } = useLocalSearchParams();
const { user } = useAuth();
const [value, setValue] = useState<string>("");
const handlerSubmitReport = async () => {
const newData = {
authorId: user?.id,
description: value,
};
try {
const response = await apiForumCreateReportCommentar({
id: id as string,
data: newData,
});
if (response.success) {
Toast.show({
type: "success",
text1: "Laporan berhasil dikirim",
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
Toast.show({
type: "error",
text1: "Gagal",
text2: "Laporan gagal dikirim",
});
}
};
const handleSubmit = ( const handleSubmit = (
<BoxButtonOnFooter> <BoxButtonOnFooter>
<ButtonCustom <ButtonCustom
disabled={!value}
backgroundColor={MainColor.red} backgroundColor={MainColor.red}
textColor={MainColor.white} textColor={MainColor.white}
onPress={() => { onPress={() => {
console.log("Report lainnya"); handlerSubmitReport();
router.back();
}} }}
> >
Report Report
</ButtonCustom> </ButtonCustom>
</BoxButtonOnFooter> </BoxButtonOnFooter>
); );
return ( return (
<> <>
<ViewWrapper footerComponent={handleSubmit}> <ViewWrapper footerComponent={handleSubmit}>
<TextAreaCustom placeholder="Laporkan Komentar" /> <TextAreaCustom
placeholder="Laporkan Komentar"
value={value}
onChangeText={setValue}
/>
</ViewWrapper> </ViewWrapper>
</> </>
); );

View File

@@ -5,17 +5,54 @@ import {
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { router } from "expo-router"; import { useAuth } from "@/hooks/use-auth";
import { apiForumCreateReportPosting } from "@/service/api-client/api-master";
import { router, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import Toast from "react-native-toast-message";
export default function ForumOtherReportPosting() { export default function ForumOtherReportPosting() {
const { id } = useLocalSearchParams();
const { user } = useAuth();
const [value, setValue] = useState<string>("");
const handlerSubmitReport = async () => {
const newData = {
authorId: user?.id,
description: value,
};
try {
const response = await apiForumCreateReportPosting({
id: id as string,
data: newData,
});
if (response.success) {
Toast.show({
type: "success",
text1: "Laporan berhasil dikirim",
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
Toast.show({
type: "error",
text1: "Gagal",
text2: "Laporan gagal dikirim",
});
}
};
const handleSubmit = ( const handleSubmit = (
<BoxButtonOnFooter> <BoxButtonOnFooter>
<ButtonCustom <ButtonCustom
disabled={!value}
backgroundColor={MainColor.red} backgroundColor={MainColor.red}
textColor={MainColor.white} textColor={MainColor.white}
onPress={() => { onPress={() => {
console.log("Report lainnya"); handlerSubmitReport();
router.back();
}} }}
> >
Report Report
@@ -25,7 +62,11 @@ export default function ForumOtherReportPosting() {
return ( return (
<> <>
<ViewWrapper footerComponent={handleSubmit}> <ViewWrapper footerComponent={handleSubmit}>
<TextAreaCustom placeholder="Laporkan Diskusi" /> <TextAreaCustom
placeholder="Laporkan Diskusi"
value={value}
onChangeText={setValue}
/>
</ViewWrapper> </ViewWrapper>
</> </>
); );

View File

@@ -1,41 +1,105 @@
import { import {
ButtonCustom, ButtonCustom,
Spacing, LoaderCustom,
StackCustom, Spacing,
ViewWrapper StackCustom,
ViewWrapper,
} from "@/components"; } from "@/components";
import { AccentColor, MainColor } from "@/constants/color-palet"; import { AccentColor, MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import Forum_ReportListSection from "@/screens/Forum/ReportListSection"; import Forum_ReportListSection from "@/screens/Forum/ReportListSection";
import { router } from "expo-router"; import { apiForumCreateReportCommentar, apiMasterForumReportList } from "@/service/api-client/api-master";
import { router, useLocalSearchParams } from "expo-router";
import { useState, useEffect } from "react";
import Toast from "react-native-toast-message";
export default function ForumReportCommentar() { export default function ForumReportCommentar() {
const { id } = useLocalSearchParams();
const { user } = useAuth();
const [selectReport, setSelectReport] = useState<string>("");
const [listMaster, setListMaster] = useState<any[] | null>(null);
const [isLoadingList, setIsLoadingList] = useState(false);
useEffect(() => {
onLoadListMaster();
}, []);
const onLoadListMaster = async () => {
try {
setIsLoadingList(true);
const response = await apiMasterForumReportList();
setListMaster(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadingList(false);
}
};
const handlerReport = async () => {
const newData = {
authorId: user?.id,
categoryId: selectReport,
};
try {
const response = await apiForumCreateReportCommentar({
id: id as string,
data: newData,
});
if (response.success) {
Toast.show({
type: "success",
text1: "Laporan berhasil dikirim",
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
Toast.show({
type: "error",
text1: "Gagal",
text2: "Laporan gagal dikirim",
});
}
};
return ( return (
<> <>
<ViewWrapper> <ViewWrapper>
<StackCustom> {isLoadingList ? (
<Forum_ReportListSection /> <LoaderCustom />
<ButtonCustom ) : (
backgroundColor={MainColor.red} <StackCustom>
textColor={MainColor.white} <Forum_ReportListSection
onPress={() => { listMaster={listMaster}
console.log("Report"); selectReport={selectReport}
router.back(); setSelectReport={setSelectReport}
}} />
> <ButtonCustom
Report disabled={!selectReport}
</ButtonCustom> backgroundColor={MainColor.red}
<ButtonCustom textColor={MainColor.white}
backgroundColor={AccentColor.blue} onPress={() => {
textColor={MainColor.white} handlerReport();
onPress={() => { }}
console.log("Lainnya"); >
router.replace("/forum/[id]/other-report-commentar"); Report
}} </ButtonCustom>
> <ButtonCustom
Lainnya backgroundColor={AccentColor.blue}
</ButtonCustom> textColor={MainColor.white}
<Spacing/> onPress={() => {
</StackCustom> router.replace(`/forum/${id}/other-report-commentar`);
}}
>
Lainnya
</ButtonCustom>
<Spacing />
</StackCustom>
)}
</ViewWrapper> </ViewWrapper>
</> </>
); );

View File

@@ -1,20 +1,103 @@
import { ViewWrapper, StackCustom, ButtonCustom, Spacing } from "@/components"; import {
import { MainColor, AccentColor } from "@/constants/color-palet"; AlertDefaultSystem,
ButtonCustom,
LoaderCustom,
Spacing,
StackCustom,
ViewWrapper,
} from "@/components";
import { AccentColor, MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth";
import Forum_ReportListSection from "@/screens/Forum/ReportListSection"; import Forum_ReportListSection from "@/screens/Forum/ReportListSection";
import { router } from "expo-router"; import {
apiForumCreateReportPosting,
apiMasterForumReportList,
} from "@/service/api-client/api-master";
import { router, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import Toast from "react-native-toast-message";
export default function ForumReportPosting() { export default function ForumReportPosting() {
return ( const { id } = useLocalSearchParams();
<> const { user } = useAuth();
<ViewWrapper> const [selectReport, setSelectReport] = useState<string>("");
const [listMaster, setListMaster] = useState<any[] | null>(null);
const [isLoadingList, setIsLoadingList] = useState(false);
useEffect(() => {
onLoadListMaster();
}, []);
const onLoadListMaster = async () => {
try {
setIsLoadingList(true);
const response = await apiMasterForumReportList();
setListMaster(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadingList(false);
}
};
const handlerReport = async () => {
const newData = {
authorId: user?.id,
categoryId: selectReport,
};
try {
const response = await apiForumCreateReportPosting({
id: id as string,
data: newData,
});
if (response.success) {
Toast.show({
type: "success",
text1: "Laporan berhasil dikirim",
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
Toast.show({
type: "error",
text1: "Gagal",
text2: "Laporan gagal dikirim",
});
}
};
return (
<>
<ViewWrapper>
{isLoadingList ? (
<LoaderCustom />
) : (
<StackCustom> <StackCustom>
<Forum_ReportListSection /> <Forum_ReportListSection
listMaster={listMaster}
selectReport={selectReport}
setSelectReport={setSelectReport}
/>
<ButtonCustom <ButtonCustom
disabled={!selectReport}
backgroundColor={MainColor.red} backgroundColor={MainColor.red}
textColor={MainColor.white} textColor={MainColor.white}
onPress={() => { onPress={() => {
console.log("Report"); AlertDefaultSystem({
router.back(); title: "Laporan Posting",
message: "Apakah anda yakin ingin melaporkan postingan ini?",
textLeft: "Batal",
textRight: "Laporkan",
onPressRight: () => {
handlerReport();
},
});
}} }}
> >
Report Report
@@ -23,15 +106,15 @@ export default function ForumReportPosting() {
backgroundColor={AccentColor.blue} backgroundColor={AccentColor.blue}
textColor={MainColor.white} textColor={MainColor.white}
onPress={() => { onPress={() => {
console.log("Lainnya"); router.replace(`/forum/${id}/other-report-posting`);
router.replace("/forum/[id]/other-report-posting");
}} }}
> >
Lainnya Lainnya
</ButtonCustom> </ButtonCustom>
<Spacing /> <Spacing />
</StackCustom> </StackCustom>
</ViewWrapper> )}
</> </ViewWrapper>
); </>
} );
}

View File

@@ -4,18 +4,47 @@ import {
TextAreaCustom, TextAreaCustom,
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { apiForumCreate } from "@/service/api-client/api-forum";
import { router } from "expo-router"; import { router } from "expo-router";
import { useState } from "react"; import { useState } from "react";
import Toast from "react-native-toast-message";
export default function ForumCreate() { export default function ForumCreate() {
const { user } = useAuth();
const [text, setText] = useState(""); const [text, setText] = useState("");
const [isLoading, setIsLoading] = useState(false);
const handlerSubmit = async () => {
const newData = {
diskusi: text,
authorId: user?.id,
};
try {
setIsLoading(true);
const response = await apiForumCreate({ data: newData });
if (response.success) {
Toast.show({
type: "success",
text1: "Posting berhasil",
});
setText("");
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
};
const buttonFooter = ( const buttonFooter = (
<BoxButtonOnFooter> <BoxButtonOnFooter>
<ButtonCustom <ButtonCustom
isLoading={isLoading}
onPress={() => { onPress={() => {
console.log("Posting", text); handlerSubmit();
router.back();
}} }}
> >
Posting Posting

View File

@@ -1,25 +1,58 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
AlertCustom, AvatarComp,
AvatarCustom,
BackButton, BackButton,
DrawerCustom, DrawerCustom,
LoaderCustom,
SearchInput, SearchInput,
TextCustom,
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import FloatingButton from "@/components/Button/FloatingButton"; import FloatingButton from "@/components/Button/FloatingButton";
import { MainColor } from "@/constants/color-palet"; import { useAuth } from "@/hooks/use-auth";
import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection"; import Forum_BoxDetailSection from "@/screens/Forum/DiscussionBoxSection";
import { listDummyDiscussionForum } from "@/screens/Forum/list-data-dummy";
import Forum_MenuDrawerBerandaSection from "@/screens/Forum/MenuDrawerSection.tsx/MenuBeranda"; import Forum_MenuDrawerBerandaSection from "@/screens/Forum/MenuDrawerSection.tsx/MenuBeranda";
import { router, Stack } from "expo-router"; import { apiForumGetAll } from "@/service/api-client/api-forum";
import { useState } from "react"; import { apiUser } from "@/service/api-client/api-user";
import { router, Stack, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function Forum() { export default function Forum() {
const id = "test-id-forum";
const [openDrawer, setOpenDrawer] = useState(false); const [openDrawer, setOpenDrawer] = useState(false);
const [status, setStatus] = useState(""); const [status, setStatus] = useState("");
const [alertStatus, setAlertStatus] = useState(false); const { user } = useAuth();
const [deleteAlert, setDeleteAlert] = useState(false); 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 ( return (
<> <>
@@ -27,12 +60,23 @@ export default function Forum() {
options={{ options={{
title: "Forum", title: "Forum",
headerLeft: () => <BackButton />, headerLeft: () => <BackButton />,
headerRight: () => <AvatarCustom href={`/forum/${id}/forumku`} />, headerRight: () => (
<AvatarComp
fileId={dataUser?.Profile?.imageId}
size="base"
href={`/forum/${user?.id}/forumku`}
/>
),
}} }}
/> />
<ViewWrapper <ViewWrapper
headerComponent={<SearchInput placeholder="Cari topik diskusi" />} headerComponent={
<SearchInput
placeholder="Cari topik diskusi"
onChangeText={(e) => setSearch(e)}
/>
}
floatingButton={ floatingButton={
<FloatingButton <FloatingButton
onPress={() => onPress={() =>
@@ -41,73 +85,45 @@ export default function Forum() {
/> />
} }
> >
{listDummyDiscussionForum.map((e, i) => ( {loadingGetList ? (
<Forum_BoxDetailSection <LoaderCustom />
key={i} ) : _.isEmpty(listData) ? (
data={e} <TextCustom align="center" color="gray">
setOpenDrawer={setOpenDrawer} Tidak ada diskusi
setStatus={setStatus} </TextCustom>
isTruncate={true} ) : (
href={`/forum/${id}`} 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> </ViewWrapper>
<DrawerCustom <DrawerCustom
height={350} height={"auto"}
isVisible={openDrawer} isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)} closeDrawer={() => setOpenDrawer(false)}
> >
<Forum_MenuDrawerBerandaSection <Forum_MenuDrawerBerandaSection
id={id} id={dataId}
authorId={authorId}
status={status} status={status}
setIsDrawerOpen={() => { setIsDrawerOpen={() => {
setOpenDrawer(false); setOpenDrawer(false);
}} }}
setShowDeleteAlert={setDeleteAlert}
setShowAlertStatus={setAlertStatus}
/> />
</DrawerCustom> </DrawerCustom>
{/* Alert Status */}
<AlertCustom
isVisible={alertStatus}
title="Ubah Status Forum"
message="Apakah Anda yakin ingin mengubah status forum ini?"
onLeftPress={() => {
setOpenDrawer(false);
setAlertStatus(false);
console.log("Batal");
}}
onRightPress={() => {
setOpenDrawer(false);
setAlertStatus(false);
console.log("Ubah status forum");
}}
textLeft="Batal"
textRight="Ubah"
colorRight={MainColor.green}
/>
{/* Alert Delete */}
<AlertCustom
isVisible={deleteAlert}
title="Hapus Forum"
message="Apakah Anda yakin ingin menghapus forum ini?"
onLeftPress={() => {
setOpenDrawer(false);
setDeleteAlert(false);
console.log("Batal");
}}
onRightPress={() => {
setOpenDrawer(false);
setDeleteAlert(false);
console.log("Hapus forum");
}}
textLeft="Batal"
textRight="Hapus"
colorRight={MainColor.red}
/>
</> </>
); );
} }

View File

@@ -1,13 +1,48 @@
import { ScrollableCustom, ViewWrapper } from "@/components"; /* eslint-disable react-hooks/exhaustive-deps */
import {
LoaderCustom,
ScrollableCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status"; import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
import Investment_StatusBox from "@/screens/Invesment/StatusBox"; import Investment_StatusBox from "@/screens/Invesment/StatusBox";
import { useState } from "react"; import { apiInvestmentGetByStatus } from "@/service/api-client/api-investment";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function InvestmentPortofolio() { export default function InvestmentPortofolio() {
const { user } = useAuth();
const [activeCategory, setActiveCategory] = useState<string | null>( const [activeCategory, setActiveCategory] = useState<string | null>(
"publish" "publish"
); );
const [listData, setListData] = useState<any[]>([]);
const [loadingList, setLoadingList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [user?.id, activeCategory])
);
const onLoadData = async () => {
try {
setLoadingList(true);
const response = await apiInvestmentGetByStatus({
authorId: user?.id as string,
status: activeCategory as string,
});
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingList(false);
}
};
const handlePress = (item: any) => { const handlePress = (item: any) => {
setActiveCategory(item.value); setActiveCategory(item.value);
// tambahkan logika lain seperti filter dsb. // tambahkan logika lain seperti filter dsb.
@@ -26,14 +61,20 @@ export default function InvestmentPortofolio() {
); );
return ( return (
<ViewWrapper headerComponent={scrollComponent} hideFooter> <ViewWrapper headerComponent={scrollComponent} hideFooter>
{Array.from({ length: 10 }).map((_, index) => ( {loadingList ? (
<Investment_StatusBox <LoaderCustom />
key={index} ) : _.isEmpty(listData) ? (
id={index.toString()} <TextCustom align="center">Tidak ada data {activeCategory}</TextCustom>
status={activeCategory as string} ) : (
href={`/investment/${index}/${activeCategory}/detail`} listData.map((item: any, index: number) => (
/> <Investment_StatusBox
))} key={index}
data={item}
status={activeCategory as string}
href={`/investment/${item.id}/${activeCategory}/detail`}
/>
))
)}
</ViewWrapper> </ViewWrapper>
); );
} }

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
BackButton, BackButton,
DotButton, DotButton,
@@ -12,16 +13,42 @@ import { ICON_SIZE_MEDIUM } from "@/constants/constans-value";
import Investment_ButtonInvestasiSection from "@/screens/Invesment/ButtonInvestasiSection"; import Investment_ButtonInvestasiSection from "@/screens/Invesment/ButtonInvestasiSection";
import Invesment_ComponentBoxOnBottomDetail from "@/screens/Invesment/ComponentBoxOnBottomDetail"; import Invesment_ComponentBoxOnBottomDetail from "@/screens/Invesment/ComponentBoxOnBottomDetail";
import Invesment_DetailDataPublishSection from "@/screens/Invesment/DetailDataPublishSection"; import Invesment_DetailDataPublishSection from "@/screens/Invesment/DetailDataPublishSection";
import { apiInvestmentGetById } from "@/service/api-client/api-investment";
import { AntDesign, MaterialIcons } from "@expo/vector-icons"; import { AntDesign, MaterialIcons } from "@expo/vector-icons";
import { router, Stack, useLocalSearchParams } from "expo-router"; import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import _ from "lodash"; import _ from "lodash";
import { useState } from "react"; import { useCallback, useState } from "react";
export default function InvestmentDetailStatus() { export default function InvestmentDetailStatus() {
const { id, status } = useLocalSearchParams(); const { id, status } = useLocalSearchParams();
const [openDrawerDraft, setOpenDrawerDraft] = useState(false); const [openDrawerDraft, setOpenDrawerDraft] = useState(false);
const [openDrawerPublish, setOpenDrawerPublish] = useState(false); const [openDrawerPublish, setOpenDrawerPublish] = useState(false);
const [data, setData] = useState<any>(null);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id, status])
);
const onLoadData = async () => {
try {
const response = await apiInvestmentGetById({
id: id as string,
});
// console.log("[DATA]", JSON.stringify(response.data, null, 2));
setData(response.data);
} catch (error) {
console.log("[ERROR]", error);
}
};
const handlePressDraft = (item: IMenuDrawerItem) => { const handlePressDraft = (item: IMenuDrawerItem) => {
console.log("PATH >> ", item.path); console.log("PATH >> ", item.path);
router.navigate(item.path as any); router.navigate(item.path as any);
@@ -63,6 +90,7 @@ export default function InvestmentDetailStatus() {
<ViewWrapper> <ViewWrapper>
<Invesment_DetailDataPublishSection <Invesment_DetailDataPublishSection
status={status as string} status={status as string}
data={data}
bottomSection={bottomSection} bottomSection={bottomSection}
buttonSection={buttonSection} buttonSection={buttonSection}
/> />

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
ButtonCenteredOnly, ButtonCenteredOnly,
ButtonCustom, ButtonCustom,
@@ -7,34 +8,192 @@ import {
Spacing, Spacing,
StackCustom, StackCustom,
TextInputCustom, TextInputCustom,
ViewWrapper ViewWrapper,
} from "@/components"; } from "@/components";
import API_STRORAGE from "@/constants/base-url-api-strorage";
import DIRECTORY_ID from "@/constants/directory-id";
import dummyPembagianDeviden from "@/lib/dummy-data/investment/pembagian-deviden"; import dummyPembagianDeviden from "@/lib/dummy-data/investment/pembagian-deviden";
import dummyListPencarianInvestor from "@/lib/dummy-data/investment/pencarian-investor"; import dummyListPencarianInvestor from "@/lib/dummy-data/investment/pencarian-investor";
import dummyPeriodeDeviden from "@/lib/dummy-data/investment/periode-deviden"; import dummyPeriodeDeviden from "@/lib/dummy-data/investment/periode-deviden";
import { router } from "expo-router"; import {
import { useState } from "react"; apiInvestmentGetById,
apiInvestmentUpdateData,
} from "@/service/api-client/api-investment";
import {
deleteImageService,
uploadImageService,
} from "@/service/upload-service";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import pickFile from "@/utils/pickFile";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
interface IInvestment {
title?: string;
targetDana?: string;
hargaLembar?: string;
totalLembar?: string;
roi?: string;
masterPencarianInvestorId?: string;
masterPeriodeDevidenId?: string;
masterPembagianDevidenId?: string;
authorId?: string;
imageId?: string;
prospektusFileId?: string;
}
export default function InvestmentEdit() { export default function InvestmentEdit() {
const [data, setData] = useState({ const { id } = useLocalSearchParams();
const [data, setData] = useState<IInvestment>({
title: "", title: "",
targetDana: 0, targetDana: "",
hargaPerLembar: 0, hargaLembar: "",
totalLembar: 0, totalLembar: "",
rasioKeuntungan: 0, roi: "",
pencarianInvestor: "", masterPencarianInvestorId: "",
periodeDeviden: "", masterPeriodeDevidenId: "",
pembagianDeviden: "", masterPembagianDevidenId: "",
authorId: "",
imageId: "",
prospektusFileId: "",
}); });
const [image, setImage] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const displayTargetDana = formatCurrencyDisplay(data?.targetDana);
const displayHargaPerLembar = formatCurrencyDisplay(data?.hargaLembar);
const displayTotalLembar = formatCurrencyDisplay(
Number(data?.targetDana) / Number(data?.hargaLembar)
);
const handleChangeCurrency = (field: keyof typeof data) => (text: string) => {
const numeric = text.replace(/\D/g, "");
setData((prev) => ({ ...prev, [field]: numeric }));
};
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
const onLoadData = async () => {
try {
const response = await apiInvestmentGetById({
id: id as string,
});
// console.log("[DATA]", JSON.stringify(response.data, null, 2));
setData(response.data);
} catch (error) {
console.log("[ERROR]", error);
}
};
const handleSubmitUpdate = async () => {
let newData = {
...data,
};
if (
newData?.title === "" ||
newData?.targetDana === "" ||
newData?.hargaLembar === "" ||
newData?.totalLembar === "" ||
newData?.roi === "" ||
newData?.masterPencarianInvestorId === "" ||
newData?.masterPeriodeDevidenId === "" ||
newData?.masterPembagianDevidenId === ""
) {
Toast.show({
type: "info",
text1: "Harap isi semua data",
});
return;
}
try {
setIsLoading(true);
if (image) {
const responseUploadImage = await uploadImageService({
imageUri: image,
dirId: DIRECTORY_ID.investasi_image,
});
if (!responseUploadImage.success) {
Toast.show({
type: "error",
text1: "Gagal mengunggah gambar",
});
return;
}
const deletePrevImage = await deleteImageService({
id: data?.imageId as any,
});
if (!deletePrevImage.success) {
Toast.show({
type: "error",
text1: "Gagal menghapus gambar",
});
return;
}
newData = {
...newData,
imageId: responseUploadImage.data.id,
};
}
const responseUpdate = await apiInvestmentUpdateData({
id: id as string,
data: newData,
});
console.log("[RESPONSE UPDATE]", JSON.parse(JSON.stringify(responseUpdate)));
if (responseUpdate.success) {
Toast.show({
type: "success",
text1: "Data berhasil diupdate",
});
router.back();
} else {
Toast.show({
type: "error",
text1: responseUpdate.message,
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
};
return ( return (
<ViewWrapper> <ViewWrapper>
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
<InformationBox text="Gambar investasi bisa berupa ilustrasi, poster atau foto terkait investasi." /> <InformationBox text="Gambar investasi bisa berupa ilustrasi, poster atau foto terkait investasi." />
<LandscapeFrameUploaded /> <LandscapeFrameUploaded
image={
image ? image : API_STRORAGE.GET({ fileId: data?.imageId as any })
}
/>
<ButtonCenteredOnly <ButtonCenteredOnly
icon="upload" icon="upload"
onPress={() => router.push("/take-picture/1")} onPress={() => {
pickFile({
setImageUri: ({ uri }) => {
console.log("URI IMAGE", uri);
setImage(uri);
},
allowedType: "image",
});
}}
> >
Upload Upload
</ButtonCenteredOnly> </ButtonCenteredOnly>
@@ -47,7 +206,7 @@ export default function InvestmentEdit() {
required required
placeholder="Judul" placeholder="Judul"
label="Judul" label="Judul"
value={data.title} value={data?.title}
onChangeText={(value) => setData({ ...data, title: value })} onChangeText={(value) => setData({ ...data, title: value })}
/> />
@@ -57,22 +216,8 @@ export default function InvestmentEdit() {
placeholder="0" placeholder="0"
label="Target Dana" label="Target Dana"
keyboardType="numeric" keyboardType="numeric"
onChangeText={(value) => onChangeText={handleChangeCurrency("targetDana")}
setData({ ...data, targetDana: Number(value) }) value={displayTargetDana}
}
value={data.targetDana === 0 ? "" : data.targetDana.toString()}
/>
<TextInputCustom
required
iconLeft="Rp."
placeholder="0"
label="Target Dana"
keyboardType="numeric"
onChangeText={(value) =>
setData({ ...data, targetDana: Number(value) })
}
value={data.targetDana === 0 ? "" : data.targetDana.toString()}
/> />
<TextInputCustom <TextInputCustom
@@ -81,10 +226,8 @@ export default function InvestmentEdit() {
placeholder="0" placeholder="0"
label="Harga Per Lembar" label="Harga Per Lembar"
keyboardType="numeric" keyboardType="numeric"
onChangeText={(value) => onChangeText={handleChangeCurrency("hargaLembar")}
setData({ ...data, targetDana: Number(value) }) value={displayHargaPerLembar}
}
value={data.targetDana === 0 ? "" : data.targetDana.toString()}
/> />
<TextInputCustom <TextInputCustom
@@ -92,10 +235,8 @@ export default function InvestmentEdit() {
placeholder="0" placeholder="0"
label="Total Lembar" label="Total Lembar"
keyboardType="numeric" keyboardType="numeric"
onChangeText={(value) => onChangeText={(value) => setData({ ...data, totalLembar: value })}
setData({ ...data, totalLembar: Number(value) }) value={displayTotalLembar}
}
value={data.totalLembar === 0 ? "" : data.totalLembar.toString()}
/> />
<TextInputCustom <TextInputCustom
@@ -104,12 +245,8 @@ export default function InvestmentEdit() {
label="Rasio Keuntungan / ROI %" label="Rasio Keuntungan / ROI %"
placeholder="0" placeholder="0"
keyboardType="numeric" keyboardType="numeric"
onChangeText={(value) => onChangeText={(value) => setData({ ...data, roi: value })}
setData({ ...data, rasioKeuntungan: Number(value) }) value={data?.roi === "" ? "" : data?.roi}
}
value={
data.rasioKeuntungan === 0 ? "" : data.rasioKeuntungan.toString()
}
/> />
<SelectCustom <SelectCustom
@@ -121,9 +258,9 @@ export default function InvestmentEdit() {
value: item.id, value: item.id,
}))} }))}
onChange={(value) => onChange={(value) =>
setData({ ...data, pencarianInvestor: value as any }) setData({ ...data, masterPencarianInvestorId: value as any })
} }
value={data.pencarianInvestor} value={data.masterPencarianInvestorId}
/> />
<SelectCustom <SelectCustom
@@ -135,9 +272,9 @@ export default function InvestmentEdit() {
value: item.id, value: item.id,
}))} }))}
onChange={(value) => onChange={(value) =>
setData({ ...data, periodeDeviden: value as any }) setData({ ...data, masterPeriodeDevidenId: value as any })
} }
value={data.periodeDeviden} value={data.masterPeriodeDevidenId}
/> />
<SelectCustom <SelectCustom
@@ -149,12 +286,12 @@ export default function InvestmentEdit() {
value: item.id, value: item.id,
}))} }))}
onChange={(value) => onChange={(value) =>
setData({ ...data, pembagianDeviden: value as any }) setData({ ...data, masterPembagianDevidenId: value as any })
} }
value={data.pembagianDeviden} value={data.masterPembagianDevidenId}
/> />
<Spacing /> <Spacing />
<ButtonCustom onPress={() => router.replace("/investment/portofolio")}> <ButtonCustom isLoading={isLoading} onPress={handleSubmitUpdate}>
Simpan Simpan
</ButtonCustom> </ButtonCustom>
</StackCustom> </StackCustom>

View File

@@ -1,76 +1,203 @@
import { import {
BaseBox, BaseBox,
ButtonCenteredOnly, ButtonCenteredOnly,
ButtonCustom, ButtonCustom,
CenterCustom, CenterCustom,
InformationBox, InformationBox,
LandscapeFrameUploaded, LandscapeFrameUploaded,
SelectCustom, SelectCustom,
Spacing, Spacing,
StackCustom, StackCustom,
TextInputCustom, TextCustom,
ViewWrapper, TextInputCustom,
ViewWrapper,
} from "@/components"; } from "@/components";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import DIRECTORY_ID from "@/constants/directory-id";
import { useAuth } from "@/hooks/use-auth";
import dummyPembagianDeviden from "@/lib/dummy-data/investment/pembagian-deviden"; import dummyPembagianDeviden from "@/lib/dummy-data/investment/pembagian-deviden";
import dummyListPencarianInvestor from "@/lib/dummy-data/investment/pencarian-investor"; import dummyListPencarianInvestor from "@/lib/dummy-data/investment/pencarian-investor";
import dummyPeriodeDeviden from "@/lib/dummy-data/investment/periode-deviden"; import dummyPeriodeDeviden from "@/lib/dummy-data/investment/periode-deviden";
import { apiInvestmentCreate } from "@/service/api-client/api-investment";
import { uploadImageService } from "@/service/upload-service";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import pickFile, { IFileData } from "@/utils/pickFile";
import { FontAwesome5 } from "@expo/vector-icons"; import { FontAwesome5 } from "@expo/vector-icons";
import { router } from "expo-router"; import { router } from "expo-router";
import { useState } from "react"; import { useState } from "react";
import Toast from "react-native-toast-message";
export default function InvestmentCreate() { export default function InvestmentCreate() {
const { user } = useAuth();
const [data, setData] = useState({ const [data, setData] = useState({
title: "", title: "",
targetDana: 0, targetDana: "",
hargaPerLembar: 0, hargaPerLembar: "",
totalLembar: 0, totalLembar: "",
rasioKeuntungan: 0, rasioKeuntungan: "",
pencarianInvestor: "", pencarianInvestor: "",
periodeDeviden: "", periodeDeviden: "",
pembagianDeviden: "", pembagianDeviden: "",
authorId: "",
imageId: "",
prospektusFileId: "",
}); });
const [image, setImage] = useState<string | null>(null);
const [pdf, setPdf] = useState<IFileData | null>(null);
const [isLoading, setIsLoading] = useState(false);
// const [coba, setCoba] = useState(""); const displayTargetDana = formatCurrencyDisplay(data.targetDana);
const displayHargaPerLembar = formatCurrencyDisplay(data.hargaPerLembar);
const displayTotalLembar = formatCurrencyDisplay(
Number(data.targetDana) / Number(data.hargaPerLembar)
);
const handleChangeCurrency = (field: keyof typeof data) => (text: string) => {
const numeric = text.replace(/\D/g, "");
setData((prev) => ({ ...prev, [field]: numeric }));
};
const handleSubmit = async () => {
if (!image || !pdf) {
Toast.show({
type: "error",
text1: "Harap pilih gambar dan file PDF",
});
return;
}
if (
!data.title ||
!data.targetDana ||
!data.hargaPerLembar ||
!data.rasioKeuntungan ||
!data.pencarianInvestor ||
!data.periodeDeviden ||
!data.pembagianDeviden
) {
Toast.show({
type: "error",
text1: "Harap isi semua data",
});
return;
}
try {
setIsLoading(true);
const responseUploadImage = await uploadImageService({
imageUri: image,
dirId: DIRECTORY_ID.investasi_image,
});
if (!responseUploadImage.success) {
Toast.show({
type: "error",
text1: "Gagal mengunggah gambar",
});
return;
}
const imageId = responseUploadImage.data.id;
const responseUploadPdf = await uploadImageService({
imageUri: pdf.uri,
dirId: DIRECTORY_ID.investasi_prospektus,
});
if (!responseUploadPdf.success) {
Toast.show({
type: "error",
text1: "Gagal mengunggah file PDF",
});
return;
}
const pdfId = responseUploadPdf.data.id;
const newData = {
title: data.title,
targetDana: data.targetDana,
hargaLembar: data.hargaPerLembar,
totalLembar: displayTotalLembar,
roi: data.rasioKeuntungan,
masterPencarianInvestorId: data.pencarianInvestor,
masterPembagianDevidenId: data.pembagianDeviden,
masterPeriodeDevidenId: data.periodeDeviden,
authorId: user?.id,
imageId: imageId,
prospektusFileId: pdfId,
};
const response = await apiInvestmentCreate({ data: newData });
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
if (response.success) {
Toast.show({
type: "success",
text1: "Berhasil",
text2: response.message,
});
router.replace("/investment/portofolio");
} else {
Toast.show({
type: "error",
text1: "Info",
text2: response.message,
});
}
} catch (error) {
console.log("error", error);
} finally {
setIsLoading(false);
}
};
// const [coba, setCoba] = useState("");
return ( return (
<ViewWrapper> <ViewWrapper>
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
{/* <View style={GStyles.inputContainerInput}>
<TextInput
style={{
...GStyles.inputText,
}}
onChangeText={(value) => setCoba(value)}
value={coba}
keyboardType="decimal-pad"
/>
</View> */}
<InformationBox text="Gambar investasi bisa berupa ilustrasi, poster atau foto terkait investasi." /> <InformationBox text="Gambar investasi bisa berupa ilustrasi, poster atau foto terkait investasi." />
<LandscapeFrameUploaded /> <LandscapeFrameUploaded image={image as string} />
<ButtonCenteredOnly <ButtonCenteredOnly
icon="upload" icon="upload"
onPress={() => router.push("/take-picture/1")} onPress={() => {
pickFile({
setImageUri: ({ uri }) => {
console.log("URI IMAGE", uri);
setImage(uri);
},
allowedType: "image",
});
}}
> >
Upload Upload
</ButtonCenteredOnly> </ButtonCenteredOnly>
<Spacing /> <Spacing />
<InformationBox text="File prospektus wajib untuk diupload, agar calon investor paham dengan prospek investasi yang akan anda jalankan kedepannya." /> <InformationBox text="File prospektus wajib untuk diupload, agar calon investor paham dengan prospek investasi yang akan anda jalankan kedepannya. Gunakan format PDF." />
<BaseBox> <BaseBox>
<CenterCustom> <CenterCustom>
<FontAwesome5 {pdf ? (
name="file-pdf" <TextCustom>{pdf.name}</TextCustom>
size={30} ) : (
color={MainColor.disabled} <FontAwesome5
/> name="file-pdf"
size={30}
color={MainColor.disabled}
/>
)}
</CenterCustom> </CenterCustom>
</BaseBox> </BaseBox>
<ButtonCenteredOnly <ButtonCenteredOnly
icon="upload" icon="upload"
onPress={() => router.push("/take-picture/1")} onPress={() => {
pickFile({
setPdfUri: ({ uri, name, size }) => {
console.log("URI PDF", JSON.stringify(uri, null, 2));
setPdf({ uri, name, size });
},
allowedType: "pdf",
});
}}
> >
Upload File Upload File
</ButtonCenteredOnly> </ButtonCenteredOnly>
@@ -90,22 +217,8 @@ export default function InvestmentCreate() {
placeholder="0" placeholder="0"
label="Target Dana" label="Target Dana"
keyboardType="numeric" keyboardType="numeric"
onChangeText={(value) => onChangeText={handleChangeCurrency("targetDana")}
setData({ ...data, targetDana: Number(value) }) value={displayTargetDana}
}
value={data.targetDana === 0 ? "" : data.targetDana.toString()}
/>
<TextInputCustom
required
iconLeft="Rp."
placeholder="0"
label="Target Dana"
keyboardType="numeric"
onChangeText={(value) =>
setData({ ...data, targetDana: Number(value) })
}
value={data.targetDana === 0 ? "" : data.targetDana.toString()}
/> />
<TextInputCustom <TextInputCustom
@@ -114,22 +227,24 @@ export default function InvestmentCreate() {
placeholder="0" placeholder="0"
label="Harga Per Lembar" label="Harga Per Lembar"
keyboardType="numeric" keyboardType="numeric"
onChangeText={(value) => onChangeText={handleChangeCurrency("hargaPerLembar")}
setData({ ...data, targetDana: Number(value) }) value={displayHargaPerLembar}
}
value={data.targetDana === 0 ? "" : data.targetDana.toString()}
/> />
<TextInputCustom <StackCustom gap={0}>
required <TextInputCustom
placeholder="0" required
label="Total Lembar" placeholder="0"
keyboardType="numeric" label="Total Lembar"
onChangeText={(value) => keyboardType="numeric"
setData({ ...data, totalLembar: Number(value) }) // onChangeText={handleChangeCurrency("totalLembar")}
} value={displayTotalLembar}
value={data.totalLembar === 0 ? "" : data.totalLembar.toString()} />
/> <TextCustom size={"small"} color="gray">
*Total lembar dihitung dari, Target Dana / Harga Perlembar
</TextCustom>
</StackCustom>
<Spacing />
<TextInputCustom <TextInputCustom
required required
@@ -137,11 +252,9 @@ export default function InvestmentCreate() {
label="Rasio Keuntungan / ROI %" label="Rasio Keuntungan / ROI %"
placeholder="0" placeholder="0"
keyboardType="numeric" keyboardType="numeric"
onChangeText={(value) => onChangeText={(value) => setData({ ...data, rasioKeuntungan: value })}
setData({ ...data, rasioKeuntungan: Number(value) })
}
value={ value={
data.rasioKeuntungan === 0 ? "" : data.rasioKeuntungan.toString() data.rasioKeuntungan === "" ? "" : data.rasioKeuntungan.toString()
} }
/> />
@@ -187,7 +300,7 @@ export default function InvestmentCreate() {
value={data.pembagianDeviden} value={data.pembagianDeviden}
/> />
<Spacing /> <Spacing />
<ButtonCustom onPress={() => router.replace("/investment/portofolio")}> <ButtonCustom isLoading={isLoading} onPress={() => handleSubmit()}>
Simpan Simpan
</ButtonCustom> </ButtonCustom>
</StackCustom> </StackCustom>

View File

@@ -1,16 +1,57 @@
import { BaseBox, TextCustom, ViewWrapper } from "@/components"; /* eslint-disable react-hooks/exhaustive-deps */
import { jobDataDummy } from "@/screens/Job/listDataDummy"; import { BaseBox, LoaderCustom, TextCustom, ViewWrapper } from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { apiJobGetAll } from "@/service/api-client/api-job";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function JobArchive() { export default function JobArchive() {
const { user } = useAuth();
const [listData, setListData] = useState<any[]>([]);
const [isLoadData, setIsLoadData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [user?.id])
);
const onLoadData = async () => {
try {
setIsLoadData(true);
const response = await apiJobGetAll({
category: "archive",
authorId: user?.id,
});
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadData(false);
}
};
return ( return (
<ViewWrapper hideFooter> <ViewWrapper hideFooter>
{jobDataDummy.map((e, i) => ( {isLoadData ? (
<BaseBox key={i} paddingTop={20} paddingBottom={20}> <LoaderCustom />
<TextCustom align="center" bold truncate size="large"> ) : _.isEmpty(listData) ? (
{e.posisi} <TextCustom align="center">Anda tidak memiliki arsip</TextCustom>
</TextCustom> ) : (
</BaseBox> listData.map((item, index) => (
))} <BaseBox
key={index}
paddingTop={20}
paddingBottom={20}
href={`/job/${item.id}/archive`}
>
<TextCustom align="center" bold truncate size="large">
{item?.title || "-"}
</TextCustom>
</BaseBox>
))
)}
</ViewWrapper> </ViewWrapper>
); );
} }

View File

@@ -28,7 +28,7 @@ export default function JobBeranda() {
const onLoadData = async (search: string) => { const onLoadData = async (search: string) => {
try { try {
setIsLoadData(true); setIsLoadData(true);
const response = await apiJobGetAll({ search }); const response = await apiJobGetAll({ search, category: "beranda" });
setListData(response.data); setListData(response.data);
} catch (error) { } catch (error) {
console.log("[ERROR]", error); console.log("[ERROR]", error);

View File

@@ -77,6 +77,7 @@ export default function JobDetailStatus() {
status={status as string} status={status as string}
isLoading={isLoading} isLoading={isLoading}
onSetLoading={setIsLoading} onSetLoading={setIsLoading}
isArchive={true}
/> />
</StackCustom> </StackCustom>
<Spacing /> <Spacing />

View File

@@ -0,0 +1,100 @@
/* eslint-disable react-hooks/exhaustive-deps */
import {
ButtonCustom,
LoaderCustom,
Spacing,
StackCustom,
ViewWrapper,
} from "@/components";
import Job_BoxDetailSection from "@/screens/Job/BoxDetailSection";
import { apiJobGetOne, apiJobUpdateData } from "@/service/api-client/api-job";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
export default function JobDetailArchive() {
const { id } = useLocalSearchParams();
const [data, setData] = useState<any>(null);
const [isLoading, setIsLoading] = useState(false);
const [isLoadData, setIsLoadData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
const onLoadData = async () => {
try {
setIsLoadData(true);
const response = await apiJobGetOne({ id: id as string });
setData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadData(false);
}
};
const handleArchive = async () => {
try {
setIsLoading(true);
const response = await apiJobUpdateData({
id: id as string,
data: false,
category: "archive",
});
if (response.success) {
Toast.show({
type: "success",
text1: response.message,
});
router.back();
} else {
Toast.show({
type: "info",
text1: "Info",
text2: response.message,
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
};
return (
<>
{isLoadData ? (
<LoaderCustom />
) : (
<ViewWrapper>
<>
<StackCustom>
<Job_BoxDetailSection data={data} />
<ButtonCustom
isLoading={isLoading}
onPress={() => {
handleArchive();
}}
>
Publish kembali
</ButtonCustom>
{/* <Job_ButtonStatusSection
id={id as string}
status={status as string}
isLoading={isLoading}
onSetLoading={setIsLoading}
isArchive={true}
/> */}
</StackCustom>
<Spacing />
</>
</ViewWrapper>
)}
</>
);
}

View File

@@ -99,6 +99,7 @@ export default function JobEdit() {
const response = await apiJobUpdateData({ const response = await apiJobUpdateData({
id: id as string, id: id as string,
data: newData, data: newData,
category: "edit",
}); });
if (response.success) { if (response.success) {

View File

@@ -1,9 +1,10 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { ButtonCustom, LoaderCustom, Spacing, ViewWrapper } from "@/components"; import { ButtonCustom, LoaderCustom, Spacing, StackCustom, ViewWrapper } from "@/components";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value"; import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import Job_BoxDetailSection from "@/screens/Job/BoxDetailSection"; import Job_BoxDetailSection from "@/screens/Job/BoxDetailSection";
import { apiJobGetOne } from "@/service/api-client/api-job"; import { apiJobGetOne } from "@/service/api-client/api-job";
import { BASE_URL } from "@/service/api-config";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import * as Clipboard from "expo-clipboard"; import * as Clipboard from "expo-clipboard";
import { useLocalSearchParams } from "expo-router"; import { useLocalSearchParams } from "expo-router";
@@ -32,7 +33,8 @@ export default function JobDetail() {
} }
}; };
const linkUrl = `http://192.168.1.83:3000/job-vacancy/`; const baseUrl = BASE_URL;
const linkUrl = `${baseUrl}/job-vacancy/`;
const OpenLinkButton = ({ id }: { id: string }) => { const OpenLinkButton = ({ id }: { id: string }) => {
const jobUrl = `${linkUrl}${id}`; const jobUrl = `${linkUrl}${id}`;
@@ -90,9 +92,11 @@ export default function JobDetail() {
) : ( ) : (
<> <>
<Job_BoxDetailSection data={data} /> <Job_BoxDetailSection data={data} />
<OpenLinkButton id={id as string} /> <StackCustom>
<OpenLinkButton id={id as string} />
<CopyLinkButton id={id as string} />
</StackCustom>
<Spacing /> <Spacing />
<CopyLinkButton id={id as string} />
</> </>
)} )}
</ViewWrapper> </ViewWrapper>

View File

@@ -1,15 +1,57 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
LoaderCustom,
TextCustom,
ViewWrapper ViewWrapper
} from "@/components"; } from "@/components";
import { useAuth } from "@/hooks/use-auth";
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection"; import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
import { apiVotingGetAll } from "@/service/api-client/api-voting";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
import { useState, useCallback } from "react";
export default function VotingContribution() { export default function VotingContribution() {
const { user } = useAuth();
const [listData, setListData] = useState<any>([]);
const [loadingGetData, setLoadingGetData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [])
);
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiVotingGetAll({
category: "contribution",
authorId: user?.id as string,
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
return ( return (
<ViewWrapper hideFooter> <ViewWrapper hideFooter>
{Array.from({ length: 5 }).map((_, index) => ( {loadingGetData ? (
<LoaderCustom />
) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada kontribusi</TextCustom>
) : listData.map((item: any, index: number) => (
<Voting_BoxPublishSection <Voting_BoxPublishSection
data={item}
key={index} key={index}
href={`/voting/${index}/contribution`} href={`/voting/${item.id}/contribution`}
/> />
))} ))}
</ViewWrapper> </ViewWrapper>

View File

@@ -1,11 +1,44 @@
import { ViewWrapper } from "@/components"; /* eslint-disable react-hooks/exhaustive-deps */
import { LoaderCustom, TextCustom, ViewWrapper } from "@/components";
import TabsTwoButtonCustom from "@/components/_ShareComponent/TabsTwoHeaderCustom"; import TabsTwoButtonCustom from "@/components/_ShareComponent/TabsTwoHeaderCustom";
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection"; import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
import { useState } from "react"; import { useAuth } from "@/hooks/use-auth";
import { useCallback, useState } from "react";
import { apiVotingGetAll } from "@/service/api-client/api-voting";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
export default function VotingHistory() { export default function VotingHistory() {
const { user } = useAuth();
const [activeCategory, setActiveCategory] = useState<string | null>("all"); const [activeCategory, setActiveCategory] = useState<string | null>("all");
const [listData, setListData] = useState<any>([]);
const [loadingGetData, setLoadingGetData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [activeCategory])
);
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiVotingGetAll({
category: activeCategory === "all" ? "all-history" : "my-history",
authorId: user?.id as string,
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
const handlePress = (item: any) => { const handlePress = (item: any) => {
setActiveCategory(item); setActiveCategory(item);
// tambahkan logika lain seperti filter dsb. // tambahkan logika lain seperti filter dsb.
@@ -25,13 +58,20 @@ export default function VotingHistory() {
/> />
} }
> >
{Array.from({ length: 10 }).map((_, index) => ( {loadingGetData ? (
<Voting_BoxPublishSection <LoaderCustom />
key={index} ) : _.isEmpty(listData) ? (
id={activeCategory as any} <TextCustom align="center">Tidak ada riwayat</TextCustom>
href={`/voting/${index}/history`} ) : (
/> listData.map((item: any, index: number) => (
))} <Voting_BoxPublishSection
key={index}
id={item.id}
data={item}
href={`/voting/${item.id}/history`}
/>
))
)}
</ViewWrapper> </ViewWrapper>
); );
} }

View File

@@ -1,23 +1,68 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
FloatingButton, FloatingButton,
LoaderCustom,
SearchInput, SearchInput,
ViewWrapper TextCustom,
ViewWrapper,
} from "@/components"; } from "@/components";
import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection"; import Voting_BoxPublishSection from "@/screens/Voting/BoxPublishSection";
import { router } from "expo-router"; import { apiVotingGetAll } from "@/service/api-client/api-voting";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function VotingBeranda() { export default function VotingBeranda() {
const [listData, setListData] = useState<any>([]);
const [loadingGetData, setLoadingGetData] = useState(false);
const [search, setSearch] = useState("");
useFocusEffect(
useCallback(() => {
onLoadData();
}, [search])
);
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiVotingGetAll({
search,
category: "beranda",
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
return ( return (
<ViewWrapper <ViewWrapper
hideFooter hideFooter
floatingButton={ floatingButton={
<FloatingButton onPress={() => router.push("/voting/create")} /> <FloatingButton onPress={() => router.push("/voting/create")} />
} }
headerComponent={<SearchInput placeholder="Cari voting" />} headerComponent={
<SearchInput placeholder="Cari voting" onChangeText={setSearch} />
}
> >
{Array.from({ length: 5 }).map((_, index) => ( {loadingGetData ? (
<Voting_BoxPublishSection key={index} href={`/voting/${index}`} /> <LoaderCustom />
))} ) : _.isEmpty(listData) ? (
<TextCustom align="center">Tidak ada data</TextCustom>
) : (
listData.map((item: any, index: number) => (
<Voting_BoxPublishSection
data={item}
key={index}
href={`/voting/${item.id}`}
/>
))
)}
</ViewWrapper> </ViewWrapper>
); );
} }

View File

@@ -1,20 +1,52 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
BadgeCustom, BadgeCustom,
BaseBox, BaseBox,
LoaderCustom,
ScrollableCustom, ScrollableCustom,
StackCustom, StackCustom,
TextCustom, TextCustom,
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import { useAuth } from "@/hooks/use-auth";
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status"; import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
import dayjs from "dayjs"; import { apiVotingGetByStatus } from "@/service/api-client/api-voting";
import { useState } from "react"; import { dateTimeView } from "@/utils/dateTimeView";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function VotingStatus() { export default function VotingStatus() {
const { user } = useAuth();
const id = user?.id || "";
const [activeCategory, setActiveCategory] = useState<string | null>( const [activeCategory, setActiveCategory] = useState<string | null>(
"publish" "publish"
); );
const [listData, setListData] = useState([]);
const [loadingGetData, setLoadingGetData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [activeCategory, id])
);
async function onLoadData() {
try {
setLoadingGetData(true);
const response = await apiVotingGetByStatus({
id: id as string,
status: activeCategory!,
});
setListData(response.data);
} catch (error) {
console.log(error);
} finally {
setLoadingGetData(false);
}
}
const handlePress = (item: any) => { const handlePress = (item: any) => {
setActiveCategory(item.value); setActiveCategory(item.value);
// tambahkan logika lain seperti filter dsb. // tambahkan logika lain seperti filter dsb.
@@ -34,27 +66,33 @@ export default function VotingStatus() {
return ( return (
<ViewWrapper headerComponent={scrollComponent} hideFooter> <ViewWrapper headerComponent={scrollComponent} hideFooter>
{Array.from({ length: 10 }).map((_, i) => ( {loadingGetData ? (
<BaseBox <LoaderCustom />
key={i} ) : _.isEmpty(listData) ? (
paddingTop={20} <TextCustom align="center">Tidak ada data {activeCategory}</TextCustom>
paddingBottom={20} ) : (
href={`/voting/${i}/${activeCategory}/detail`} listData.map((item: any, i: number) => (
> <BaseBox
<StackCustom> key={i}
<TextCustom align="center" bold truncate size="large"> paddingTop={20}
Lorem ipsum dolor sit {activeCategory} paddingBottom={20}
</TextCustom> href={`/voting/${item.id}/${activeCategory}/detail`}
<BadgeCustom >
style={{ width: "70%", alignSelf: "center" }} <StackCustom>
variant="light" <TextCustom align="center" bold truncate={2} size="large">
> {item?.title || ""}
{dayjs().format("DD/MM/YYYY")} -{" "} </TextCustom>
{dayjs().add(1, "day").format("DD/MM/YYYY")} <BadgeCustom
</BadgeCustom> style={{ width: "70%", alignSelf: "center" }}
</StackCustom> variant="light"
</BaseBox> >
))} {item?.awalVote && dateTimeView({date: item?.awalVote, withoutTime: true})} -{" "}
{item?.akhirVote && dateTimeView({date: item?.akhirVote, withoutTime: true})}
</BadgeCustom>
</StackCustom>
</BaseBox>
))
)}
</ViewWrapper> </ViewWrapper>
); );
} }

View File

@@ -1,23 +1,63 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
AlertDefaultSystem, AlertDefaultSystem,
BackButton, BackButton,
BaseBox,
DotButton, DotButton,
DrawerCustom, DrawerCustom,
LoaderCustom,
MenuDrawerDynamicGrid, MenuDrawerDynamicGrid,
Spacing, Spacing,
TextCustom,
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import { IconArchive, IconContribution, IconEdit } from "@/components/_Icon"; import { IconArchive, IconContribution, IconEdit } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types"; import { IMenuDrawerItem } from "@/components/_Interface/types";
import Voting_BoxDetailHasilVotingSection from "@/screens/Voting/BoxDetailHasilVotingSection";
import { Voting_BoxDetailSection } from "@/screens/Voting/BoxDetailSection"; import { Voting_BoxDetailSection } from "@/screens/Voting/BoxDetailSection";
import Voting_ButtonStatusSection from "@/screens/Voting/ButtonStatusSection"; import Voting_ButtonStatusSection from "@/screens/Voting/ButtonStatusSection";
import { router, Stack, useLocalSearchParams } from "expo-router"; import {
import { useState } from "react"; apiVotingGetOne,
apiVotingUpdateData,
} from "@/service/api-client/api-voting";
import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
export default function VotingDetailStatus() { export default function VotingDetailStatus() {
const { id, status } = useLocalSearchParams(); const { id, status } = useLocalSearchParams();
const [openDrawerDraft, setOpenDrawerDraft] = useState(false); const [openDrawerDraft, setOpenDrawerDraft] = useState(false);
const [openDrawerPublish, setOpenDrawerPublish] = useState(false); const [openDrawerPublish, setOpenDrawerPublish] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [loadingGetData, setLoadingGetData] = useState(false);
const [data, setData] = useState<any>(null);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiVotingGetOne({ id: id as string });
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
const handlePressDraft = (item: IMenuDrawerItem) => { const handlePressDraft = (item: IMenuDrawerItem) => {
console.log("PATH >> ", item.path); console.log("PATH >> ", item.path);
@@ -32,9 +72,24 @@ export default function VotingDetailStatus() {
message: "Apakah Anda yakin ingin mengarsipkan voting ini?", message: "Apakah Anda yakin ingin mengarsipkan voting ini?",
textLeft: "Batal", textLeft: "Batal",
textRight: "Ya", textRight: "Ya",
onPressRight: () => { onPressRight: async () => {
console.log("Hapus"); try {
router.back(); const response = await apiVotingUpdateData({
id: id as string,
data: data.isArsip ? false : true,
category: "archive",
});
if (response.success) {
Toast.show({
type: "success",
text1: response.message,
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
}
}, },
}); });
} }
@@ -46,7 +101,7 @@ export default function VotingDetailStatus() {
<> <>
<Stack.Screen <Stack.Screen
options={{ options={{
title: `Detail ${status}`, title: `Detail`,
headerLeft: () => <BackButton />, headerLeft: () => <BackButton />,
headerRight: () => headerRight: () =>
status === "draft" ? ( status === "draft" ? (
@@ -57,9 +112,37 @@ export default function VotingDetailStatus() {
}} }}
/> />
<ViewWrapper> <ViewWrapper>
<Voting_BoxDetailSection /> {loadingGetData ? (
<Voting_ButtonStatusSection status={status as string} /> <LoaderCustom />
<Spacing /> ) : (
<>
{status === "publish" && (
<BaseBox>
<TextCustom bold>
Status:{" "}
<TextCustom color={data?.isArsip ? "red" : "green"}>
{data?.isArsip ? "Arsip" : "Publish"}
</TextCustom>
</TextCustom>
</BaseBox>
)}
<Spacing height={0} />
<Voting_BoxDetailSection data={data as any} />
{status === "publish" ? (
<Voting_BoxDetailHasilVotingSection
listData={data?.Voting_DaftarNamaVote}
/>
) : (
<Voting_ButtonStatusSection
isLoading={isLoading}
onSetLoading={setIsLoading}
id={id as string}
status={status as string}
/>
)}
<Spacing />
</>
)}
</ViewWrapper> </ViewWrapper>
{/* ========= Draft Drawer ========= */} {/* ========= Draft Drawer ========= */}

View File

@@ -1,22 +1,76 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
AvatarUsernameAndOtherComponent, AvatarUsernameAndOtherComponent,
BackButton, BackButton,
DotButton, DotButton,
DrawerCustom, DrawerCustom,
MenuDrawerDynamicGrid, LoaderCustom,
Spacing, MenuDrawerDynamicGrid,
ViewWrapper, Spacing,
ViewWrapper,
} from "@/components"; } from "@/components";
import { IconContribution } from "@/components/_Icon"; import { IconContribution } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types"; import { IMenuDrawerItem } from "@/components/_Interface/types";
import { useAuth } from "@/hooks/use-auth";
import { Voting_BoxDetailContributionSection } from "@/screens/Voting/BoxDetailContribution"; import { Voting_BoxDetailContributionSection } from "@/screens/Voting/BoxDetailContribution";
import Voting_BoxDetailHasilVotingSection from "@/screens/Voting/BoxDetailHasilVotingSection"; import Voting_BoxDetailHasilVotingSection from "@/screens/Voting/BoxDetailHasilVotingSection";
import {
apiVotingContribution,
apiVotingGetOne,
} from "@/service/api-client/api-voting";
import { router, Stack, useLocalSearchParams } from "expo-router"; import { router, Stack, useLocalSearchParams } from "expo-router";
import { useState } from "react"; import { useEffect, useState } from "react";
export default function VotingDetailContribution() { export default function VotingDetailContribution() {
const { user } = useAuth();
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const [openDrawerPublish, setOpenDrawerPublish] = useState(false); const [openDrawerPublish, setOpenDrawerPublish] = useState(false);
const [data, setData] = useState<any>(null);
const [loadingGetData, setLoadingGetData] = useState(false);
const [nameChoice, setNameChoice] = useState("");
useEffect(() => {
handlerLoadData();
}, [id, user?.id]);
async function handlerLoadData() {
try {
setLoadingGetData(true);
await onLoadData();
await onLoadCheckContribution();
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
}
const onLoadData = async () => {
try {
const response = await apiVotingGetOne({ id: id as string });
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
const onLoadCheckContribution = async () => {
try {
const response = await apiVotingContribution({
id: id as string,
authorId: user?.id as string,
category: "checked",
});
if (response.success) {
setNameChoice(response.data.nameChoice);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
const handlePressPublish = (item: IMenuDrawerItem) => { const handlePressPublish = (item: IMenuDrawerItem) => {
router.navigate(item.path as any); router.navigate(item.path as any);
@@ -36,11 +90,27 @@ export default function VotingDetailContribution() {
/> />
<ViewWrapper> <ViewWrapper>
<Voting_BoxDetailContributionSection {loadingGetData ? (
headerAvatar={<AvatarUsernameAndOtherComponent />} <LoaderCustom />
/> ) : (
<Voting_BoxDetailHasilVotingSection /> <>
<Spacing /> <Voting_BoxDetailContributionSection
data={data}
nameChoice={nameChoice}
headerAvatar={
<AvatarUsernameAndOtherComponent
avatar={data?.Author?.Profile?.imageId || ""}
name={data?.Author?.username || "Username"}
avatarHref={`/profile/${data?.Author?.Profile?.id}`}
/>
}
/>
<Voting_BoxDetailHasilVotingSection
listData={data?.Voting_DaftarNamaVote}
/>
<Spacing />
</>
)}
</ViewWrapper> </ViewWrapper>
{/* ========= Publish Drawer ========= */} {/* ========= Publish Drawer ========= */}

View File

@@ -1,29 +1,184 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
ActionIcon,
BoxButtonOnFooter, BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom, ButtonCustom,
Grid, CenterCustom,
LoaderCustom,
Spacing, Spacing,
StackCustom, StackCustom,
TextAreaCustom, TextAreaCustom,
TextCustom,
TextInputCustom, TextInputCustom,
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom"; import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
import {
apiVotingGetOne,
apiVotingUpdateData,
} from "@/service/api-client/api-voting";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { router } from "expo-router"; import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { TouchableOpacity } from "react-native"; import _ from "lodash";
import { useCallback, useState } from "react";
import { View } from "react-native";
import Toast from "react-native-toast-message";
interface IEditData {
title?: string;
deskripsi?: string;
awalVote?: string;
akhirVote?: string;
Voting_DaftarNamaVote?: [
{
value?: string;
}
];
}
export default function VotingEdit() { export default function VotingEdit() {
const { id } = useLocalSearchParams();
const [loadingGetData, setLoadingGetData] = useState(false);
const [data, setData] = useState<IEditData>();
const [isLoading, setIsLoading] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiVotingGetOne({ id: id as string });
if (response.success) {
const data = response.data;
setData({
title: data.title,
deskripsi: data.deskripsi,
awalVote: data.awalVote,
akhirVote: data.akhirVote,
Voting_DaftarNamaVote: data.Voting_DaftarNamaVote,
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
};
const validateDateRange = ({
selectedStratDate,
selectedEndDate,
}: {
selectedStratDate: string | Date;
selectedEndDate: string | Date;
}): { isValid: boolean; error?: string } => {
const startDate = new Date(selectedStratDate);
const endDate = new Date(selectedEndDate);
// Cek apakah tanggal valid
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
return {
isValid: false,
error: "Invalid date provided",
};
}
if (startDate >= endDate) {
return {
isValid: false,
error: "Ubah tanggal berakhirnya event",
};
}
return {
isValid: true,
error: undefined,
};
};
const validateForm = async () => {
if (!data?.title || !data?.deskripsi) {
Toast.show({
type: "info",
text1: "Lengkapi semua data",
});
return false;
}
if (data?.Voting_DaftarNamaVote?.some((item: any) => item.value === "")) {
Toast.show({
type: "info",
text1: "Isi semua data pilihan",
});
return false;
}
const startDate = new Date(data?.awalVote as any);
const endDate = new Date(data?.akhirVote as any);
if (startDate >= endDate) {
Toast.show({
type: "info",
text1: "Ubah tanggal berakhirnya event",
});
return false;
}
return true;
};
const handlerUpdateSubmit = async () => {
const isValid = await validateForm();
if (!isValid) return;
try {
setIsLoading(true);
const newData = {
...data,
awalVote: new Date(data?.awalVote as any).toISOString(),
akhirVote: new Date(data?.akhirVote as any).toISOString(),
listVote: data?.Voting_DaftarNamaVote?.map((item: any) => item.value),
};
const response = await apiVotingUpdateData({
id: id as string,
data: newData,
category: "edit",
});
if (response.success) {
Toast.show({
type: "success",
text1: response.message,
});
return router.back();
} else {
Toast.show({
type: "error",
text1: response.message,
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
};
const buttonSubmit = () => { const buttonSubmit = () => {
return ( return (
<> <>
<BoxButtonOnFooter> <BoxButtonOnFooter>
<ButtonCustom <ButtonCustom
onPress={() => isLoading={isLoading}
router.back() onPress={() => handlerUpdateSubmit()}
}
> >
Update Update
</ButtonCustom> </ButtonCustom>
@@ -34,45 +189,144 @@ export default function VotingEdit() {
return ( return (
<ViewWrapper footerComponent={buttonSubmit()}> <ViewWrapper footerComponent={buttonSubmit()}>
<StackCustom gap={"xs"}> {loadingGetData ? (
<TextInputCustom <LoaderCustom />
label="Judul Voting" ) : (
placeholder="MasukanJudul Voting" <StackCustom gap={"xs"}>
required <TextInputCustom
/> label="Judul Voting"
<TextAreaCustom placeholder="MasukanJudul Voting"
label="Deskripsi" required
placeholder="Masukan Deskripsi" value={data?.title}
required onChangeText={(text) => setData({ ...data, title: text })}
showCount />
maxLength={1000} <TextAreaCustom
/> label="Deskripsi"
<DateTimePickerCustom label="Mulai Voting" required /> placeholder="Masukan Deskripsi"
<DateTimePickerCustom label="Voting Berakhir" required /> required
showCount
maxLength={1000}
value={data?.deskripsi}
onChangeText={(text) => setData({ ...data, deskripsi: text })}
/>
<Grid> <Spacing />
<Grid.Col span={10}>
<DateTimePickerCustom
minimumDate={new Date(Date.now())}
label="Mulai Voting"
required
value={new Date(data?.awalVote as any)}
onChange={(date: any) => {
setData({ ...data, awalVote: date });
}}
/>
<StackCustom gap={0}>
<DateTimePickerCustom
minimumDate={new Date(data?.awalVote as any)}
label="Voting Berakhir"
required
value={new Date(data?.akhirVote as any)}
onChange={(date: any) => {
setData({ ...data, akhirVote: date });
}}
/>
{validateDateRange({
selectedStratDate: data?.awalVote as any,
selectedEndDate: data?.akhirVote as any,
}).isValid ? (
<TextCustom style={{ color: "green" }}>
{
validateDateRange({
selectedStratDate: data?.awalVote as any,
selectedEndDate: data?.akhirVote as any,
}).error
}
</TextCustom>
) : (
<TextCustom style={{ color: "red" }}>
{
validateDateRange({
selectedStratDate: data?.awalVote as any,
selectedEndDate: data?.akhirVote as any,
}).error
}
</TextCustom>
)}
<Spacing />
</StackCustom>
{data?.Voting_DaftarNamaVote?.map((item: any, index: number) => (
<TextInputCustom <TextInputCustom
key={index}
label="Pilihan" label="Pilihan"
placeholder="Masukan Pilihan" placeholder="Masukan Pilihan"
required required
value={item.value}
onChangeText={(value: any) =>
setData({
...(data as any),
Voting_DaftarNamaVote: data?.Voting_DaftarNamaVote?.map(
(item: any, i: any) =>
i === index ? { ...item, value } : item
),
})
}
/> />
</Grid.Col> ))}
<Grid.Col
span={2}
style={{ alignItems: "center", justifyContent: "center" }}
>
<TouchableOpacity onPress={() => console.log("delete")}>
<Ionicons name="trash" size={24} color={MainColor.red} />
</TouchableOpacity>
</Grid.Col>
</Grid>
<ButtonCenteredOnly onPress={() => console.log("add")}> <CenterCustom>
Tambah Pilihan <View
</ButtonCenteredOnly> style={{ flexDirection: "row", alignItems: "center", gap: 10 }}
<Spacing /> >
</StackCustom> <ActionIcon
disabled={(data as any)?.Voting_DaftarNamaVote?.length >= 4}
onPress={() => {
setData({
...(data as any),
Voting_DaftarNamaVote: [
...(data as any)?.Voting_DaftarNamaVote,
{ value: "" },
],
});
}}
icon={
<Ionicons
name="add-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
<ActionIcon
disabled={
((data as any)?.Voting_DaftarNamaVote?.length as any) <= 2
}
onPress={() => {
const list = _.clone((data as any)?.Voting_DaftarNamaVote);
list.pop();
setData({
...(data as any),
Voting_DaftarNamaVote: list,
});
}}
icon={
<Ionicons
name="remove-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
</View>
</CenterCustom>
<Spacing />
</StackCustom>
)}
</ViewWrapper> </ViewWrapper>
); );
} }

View File

@@ -1,23 +1,78 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
AvatarUsernameAndOtherComponent, AvatarUsernameAndOtherComponent,
BackButton, BackButton,
DotButton, DotButton,
DrawerCustom, DrawerCustom,
MenuDrawerDynamicGrid, LoaderCustom,
Spacing, MenuDrawerDynamicGrid,
ViewWrapper, Spacing,
ViewWrapper,
} from "@/components"; } from "@/components";
import { IconContribution } from "@/components/_Icon"; import { IconContribution } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types"; import { IMenuDrawerItem } from "@/components/_Interface/types";
import { useAuth } from "@/hooks/use-auth";
import Voting_BoxDetailHasilVotingSection from "@/screens/Voting/BoxDetailHasilVotingSection"; import Voting_BoxDetailHasilVotingSection from "@/screens/Voting/BoxDetailHasilVotingSection";
import { Voting_BoxDetailHistorySection } from "@/screens/Voting/BoxDetailHistorySection"; import { Voting_BoxDetailHistorySection } from "@/screens/Voting/BoxDetailHistorySection";
import {
apiVotingContribution,
apiVotingGetOne,
} from "@/service/api-client/api-voting";
import { router, Stack, useLocalSearchParams } from "expo-router"; import { router, Stack, useLocalSearchParams } from "expo-router";
import { useState } from "react"; import { useEffect, useState } from "react";
export default function VotingDetailHistory() { export default function VotingDetailHistory() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const { user } = useAuth();
const [openDrawerPublish, setOpenDrawerPublish] = useState(false); const [openDrawerPublish, setOpenDrawerPublish] = useState(false);
const [data, setData] = useState<any>(null);
const [loadingGetData, setLoadingGetData] = useState(false);
const [nameChoice, setNameChoice] = useState("");
useEffect(() => {
handlerLoadData();
}, [id, user?.id]);
async function handlerLoadData() {
try {
setLoadingGetData(true);
await onLoadData();
await onLoadCheckContribution();
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
}
const onLoadData = async () => {
try {
const response = await apiVotingGetOne({ id: id as string });
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
const onLoadCheckContribution = async () => {
try {
const response = await apiVotingContribution({
id: id as string,
authorId: user?.id as string,
category: "checked",
});
if (response.success) {
setNameChoice(response.data.nameChoice);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
const handlePressPublish = (item: IMenuDrawerItem) => { const handlePressPublish = (item: IMenuDrawerItem) => {
router.navigate(item.path as any); router.navigate(item.path as any);
setOpenDrawerPublish(false); setOpenDrawerPublish(false);
@@ -35,11 +90,27 @@ export default function VotingDetailHistory() {
}} }}
/> />
<ViewWrapper> <ViewWrapper>
<Voting_BoxDetailHistorySection {loadingGetData ? (
headerAvatar={<AvatarUsernameAndOtherComponent />} <LoaderCustom />
/> ) : (
<Voting_BoxDetailHasilVotingSection /> <>
<Spacing /> <Voting_BoxDetailHistorySection
data={data}
nameChoice={nameChoice}
headerAvatar={
<AvatarUsernameAndOtherComponent
avatar={data?.Author?.Profile?.imageId || ""}
name={data?.Author?.username || "Username"}
avatarHref={`/profile/${data?.Author?.Profile?.id}`}
/>
}
/>
<Voting_BoxDetailHasilVotingSection
listData={data?.Voting_DaftarNamaVote}
/>
<Spacing />
</>
)}
</ViewWrapper> </ViewWrapper>
{/* ========= Publish Drawer ========= */} {/* ========= Publish Drawer ========= */}

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
AlertDefaultSystem, AlertDefaultSystem,
AvatarUsernameAndOtherComponent, AvatarUsernameAndOtherComponent,
@@ -5,20 +6,87 @@ import {
DotButton, DotButton,
DrawerCustom, DrawerCustom,
InformationBox, InformationBox,
LoaderCustom,
MenuDrawerDynamicGrid, MenuDrawerDynamicGrid,
StackCustom, StackCustom,
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import { IconArchive, IconContribution } from "@/components/_Icon"; import { IconArchive, IconContribution } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types"; import { IMenuDrawerItem } from "@/components/_Interface/types";
import { useAuth } from "@/hooks/use-auth";
import Voting_BoxDetailHasilVotingSection from "@/screens/Voting/BoxDetailHasilVotingSection"; import Voting_BoxDetailHasilVotingSection from "@/screens/Voting/BoxDetailHasilVotingSection";
import { Voting_BoxDetailPublishSection } from "@/screens/Voting/BoxDetailPublishSection"; import { Voting_BoxDetailPublishSection } from "@/screens/Voting/BoxDetailPublishSection";
import { router, Stack, useLocalSearchParams } from "expo-router"; import {
import React, { useState } from "react"; apiVotingContribution,
apiVotingGetOne,
apiVotingUpdateData,
} from "@/service/api-client/api-voting";
import { today } from "@/utils/dateTimeView";
import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import React, { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
export default function VotingDetail() { export default function VotingDetail() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const { user } = useAuth();
const [openDrawerPublish, setOpenDrawerPublish] = useState(false); const [openDrawerPublish, setOpenDrawerPublish] = useState(false);
const [data, setData] = useState<any>(null);
const [loadingGetData, setLoadingGetData] = useState(false);
const [isContribution, setIsContribution] = useState(false);
const [nameChoice, setNameChoice] = useState("");
useFocusEffect(
useCallback(() => {
handlerLoadData();
}, [id, user?.id])
);
async function handlerLoadData() {
try {
setLoadingGetData(true);
await onLoadData();
await onLoadCheckContribution();
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingGetData(false);
}
}
const onLoadData = async () => {
try {
const response = await apiVotingGetOne({ id: id as string });
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
const onLoadCheckContribution = async () => {
try {
const response = await apiVotingContribution({
id: id as string,
authorId: user?.id as string,
category: "checked",
});
if (response.success) {
setIsContribution(response.data.isContribution);
setNameChoice(response.data.nameChoice);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
const handlePressPublish = (item: IMenuDrawerItem) => { const handlePressPublish = (item: IMenuDrawerItem) => {
if (item.path === "") { if (item.path === "") {
AlertDefaultSystem({ AlertDefaultSystem({
@@ -26,9 +94,24 @@ export default function VotingDetail() {
message: "Apakah Anda yakin ingin mengarsipkan voting ini?", message: "Apakah Anda yakin ingin mengarsipkan voting ini?",
textLeft: "Batal", textLeft: "Batal",
textRight: "Ya", textRight: "Ya",
onPressRight: () => { onPressRight: async () => {
console.log("Hapus"); try {
router.back(); const response = await apiVotingUpdateData({
id: id as string,
data: data.isArsip ? false : true,
category: "archive",
});
if (response.success) {
Toast.show({
type: "success",
text1: response.message,
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
}
}, },
}); });
} }
@@ -49,15 +132,32 @@ export default function VotingDetail() {
/> />
<ViewWrapper> <ViewWrapper>
<StackCustom> {loadingGetData ? (
<InformationBox text="Untuk sementara voting ini belum di buka. Voting akan dimulai sesuai dengan tanggal awal pemilihan, dan akan ditutup sesuai dengan tanggal akhir pemilihan." /> <LoaderCustom />
) : (
<StackCustom gap={"xs"}>
{today.getDate() < new Date(data?.awalVote).getDate() && (
<InformationBox text="Untuk sementara voting tidak dapat dilakukan. Voting dapat dimulai sesuai dengan tanggal awal pemilihan, dan akan ditutup sesuai dengan tanggal akhir pemilihan." />
)}
<Voting_BoxDetailPublishSection
data={data}
userId={user?.id as string}
isContribution={isContribution}
nameChoice={nameChoice}
headerAvatar={
<AvatarUsernameAndOtherComponent
avatar={data?.Author?.Profile?.imageId || ""}
name={data?.Author?.username || "Username"}
avatarHref={`/profile/${data?.Author?.Profile?.id}`}
/>
}
/>
<Voting_BoxDetailPublishSection <Voting_BoxDetailHasilVotingSection
headerAvatar={<AvatarUsernameAndOtherComponent />} listData={data?.Voting_DaftarNamaVote}
/> />
</StackCustom>
<Voting_BoxDetailHasilVotingSection /> )}
</StackCustom>
</ViewWrapper> </ViewWrapper>
{/* ========= Publish Drawer ========= */} {/* ========= Publish Drawer ========= */}
@@ -67,18 +167,28 @@ export default function VotingDetail() {
height={"auto"} height={"auto"}
> >
<MenuDrawerDynamicGrid <MenuDrawerDynamicGrid
data={[ data={
{ user?.id === data?.Author?.id
icon: <IconContribution />, ? [
label: "Daftar Kontributor", {
path: `/voting/${id}/list-of-contributor`, icon: <IconContribution />,
}, label: "Daftar Kontributor",
{ path: `/voting/${id}/list-of-contributor`,
icon: <IconArchive />, },
label: "Update Arsip", {
path: "" as any, icon: <IconArchive />,
}, label: "Update Arsip",
]} path: "" as any,
},
]
: [
{
icon: <IconContribution />,
label: "Daftar Kontributor",
path: `/voting/${id}/list-of-contributor`,
},
]
}
onPressItem={handlePressPublish as any} onPressItem={handlePressPublish as any}
/> />
</DrawerCustom> </DrawerCustom>

View File

@@ -1,26 +1,69 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
AvatarUsernameAndOtherComponent, AvatarUsernameAndOtherComponent,
BadgeCustom, BadgeCustom,
BaseBox, BaseBox,
LoaderCustom,
TextCustom,
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import { apiVotingContribution } from "@/service/api-client/api-voting";
import { useFocusEffect, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function Voting_ListOfContributor() { export default function Voting_ListOfContributor() {
const { id } = useLocalSearchParams();
const [listData, setListData] = useState<any>([]);
const [isLoadData, setIsLoadData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadList();
}, [id])
);
const onLoadList = async () => {
try {
setIsLoadData(true);
const response = await apiVotingContribution({
id: id as string,
authorId: "",
category: "list",
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadData(false);
}
};
return ( return (
<ViewWrapper> <ViewWrapper>
{Array.from({ length: 10 }).map((_, index) => ( {isLoadData ? (
<BaseBox paddingTop={5} paddingBottom={5} key={index.toString()}> <LoaderCustom />
<AvatarUsernameAndOtherComponent ) : _.isEmpty(listData) ? (
rightComponent={ <TextCustom align="center">Tidak ada kontributor</TextCustom>
<BadgeCustom ) : (
style={{alignSelf: "flex-end" }} listData.map((item: any, index: number) => (
> <BaseBox paddingTop={5} paddingBottom={5} key={index.toString()}>
Pilihan {index + 1} <AvatarUsernameAndOtherComponent
</BadgeCustom> avatar={item?.Author?.Profile?.imageId || ""}
} name={item?.Author?.username || "Username"}
/> avatarHref={`/profile/${item?.Author?.Profile?.id}`}
</BaseBox> rightComponent={
))} <BadgeCustom style={{ alignSelf: "flex-end" }}>
{item?.Voting_DaftarNamaVote?.value}
</BadgeCustom>
}
/>
</BaseBox>
))
)}
</ViewWrapper> </ViewWrapper>
); );
} }

View File

@@ -1,30 +1,103 @@
import { import {
ActionIcon,
BoxButtonOnFooter, BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom, ButtonCustom,
Grid, CenterCustom,
Spacing, Spacing,
StackCustom, StackCustom,
TextAreaCustom, TextAreaCustom,
TextInputCustom, TextInputCustom,
ViewWrapper, ViewWrapper
} from "@/components"; } from "@/components";
import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom"; import DateTimePickerCustom from "@/components/DateInput/DateTimePickerCustom";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";
import { apiVotingCreate } from "@/service/api-client/api-voting";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { router } from "expo-router"; import { router } from "expo-router";
import { TouchableOpacity } from "react-native"; import _ from "lodash";
import { useState } from "react";
import { View } from "react-native";
import Toast from "react-native-toast-message";
export default function VotingCreate() { export default function VotingCreate() {
const { user } = useAuth();
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState({
authorId: "",
title: "",
deskripsi: "",
awalVote: "",
akhirVote: "",
listVote: [],
});
const [listVote, setListVote] = useState([
{
name: "Nama Pilihan",
value: "",
},
{
name: "Nama Pilihan",
value: "",
},
]);
const handlerSubmit = async () => {
if (!data.title || !data.deskripsi || !data.awalVote || !data.akhirVote) {
Toast.show({
type: "info",
text1: "Lengkapi semua data",
});
return;
}
if (listVote.some((item: any) => item.value === "")) {
Toast.show({
type: "info",
text1: "Lengkapi semua data pilihan",
});
return;
}
try {
setIsLoading(true);
const newData = {
...data,
authorId: user?.id,
awalVote: new Date(data.awalVote as any).toISOString(),
akhirVote: new Date(data.akhirVote as any).toISOString(),
listVote: listVote,
};
const response = await apiVotingCreate(newData);
// console.log("[RESPONSE]", JSON.stringify(response, null, 2));
if (response.success) {
Toast.show({
type: "success",
text1: "Data berhasil disimpan",
});
router.replace("/(application)/(user)/voting/(tabs)/status");
} else {
Toast.show({
type: "error",
text1: "Data gagal disimpan",
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
};
const buttonSubmit = () => { const buttonSubmit = () => {
return ( return (
<> <>
<BoxButtonOnFooter> <BoxButtonOnFooter>
<ButtonCustom <ButtonCustom isLoading={isLoading} onPress={() => handlerSubmit()}>
onPress={() =>
router.replace("/(application)/(user)/voting/(tabs)/status")
}
>
Simpan Simpan
</ButtonCustom> </ButtonCustom>
</BoxButtonOnFooter> </BoxButtonOnFooter>
@@ -39,6 +112,8 @@ export default function VotingCreate() {
label="Judul Voting" label="Judul Voting"
placeholder="MasukanJudul Voting" placeholder="MasukanJudul Voting"
required required
value={data.title}
onChangeText={(value: any) => setData({ ...data, title: value })}
/> />
<TextAreaCustom <TextAreaCustom
label="Deskripsi" label="Deskripsi"
@@ -46,31 +121,81 @@ export default function VotingCreate() {
required required
showCount showCount
maxLength={1000} maxLength={1000}
value={data.deskripsi}
onChangeText={(value: any) => setData({ ...data, deskripsi: value })}
/>
<DateTimePickerCustom
label="Mulai Voting"
required
// value={data.awalVote ? new Date(data.awalVote) : null}
onChange={(value: any) => setData({ ...data, awalVote: value })}
minimumDate={new Date(Date.now())}
/>
<DateTimePickerCustom
disabled={!data.awalVote}
label="Voting Berakhir"
required
// value={data.akhirVote ? new Date(data.akhirVote) : null}
onChange={(value: any) => setData({ ...data, akhirVote: value })}
minimumDate={
data.awalVote ? new Date(data.awalVote) : new Date(Date.now())
}
/> />
<DateTimePickerCustom label="Mulai Voting" required />
<DateTimePickerCustom label="Voting Berakhir" required />
<Grid>
<Grid.Col span={10}> {listVote.map((item, index) => (
<TextInputCustom <TextInputCustom
label="Pilihan" key={index}
placeholder="Masukan Pilihan" label="Pilihan"
required placeholder="Masukan Pilihan"
required
value={item.value}
onChangeText={(value: any) =>
setListVote(
listVote.map((item, i) =>
i === index ? { ...item, value } : item
)
)
}
/>
))}
<CenterCustom>
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
<ActionIcon
disabled={listVote.length >= 4}
onPress={() => {
setListVote([...listVote, { name: "Nama Pilihan", value: "" }]);
}}
icon={
<Ionicons
name="add-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/> />
</Grid.Col> <ActionIcon
<Grid.Col disabled={listVote.length <= 2}
span={2} onPress={() => {
style={{ alignItems: "center", justifyContent: "center" }} const list = _.clone(listVote);
> list.pop();
<TouchableOpacity onPress={() => console.log("delete")}> setListVote(list);
<Ionicons name="trash" size={24} color={MainColor.red} /> }}
</TouchableOpacity> icon={
</Grid.Col> <Ionicons
</Grid> name="remove-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
</View>
</CenterCustom>
<Spacing />
<ButtonCenteredOnly onPress={() => console.log("add")}>
Tambah Pilihan
</ButtonCenteredOnly>
<Spacing /> <Spacing />
</StackCustom> </StackCustom>
</ViewWrapper> </ViewWrapper>

View File

@@ -42,6 +42,7 @@
"react-native-dotenv": "^3.4.11", "react-native-dotenv": "^3.4.11",
"react-native-gesture-handler": "~2.28.0", "react-native-gesture-handler": "~2.28.0",
"react-native-international-phone-number": "^0.9.3", "react-native-international-phone-number": "^0.9.3",
"react-native-keyboard-controller": "^1.18.6",
"react-native-maps": "1.20.1", "react-native-maps": "1.20.1",
"react-native-otp-entry": "^1.8.5", "react-native-otp-entry": "^1.8.5",
"react-native-pager-view": "6.9.1", "react-native-pager-view": "6.9.1",
@@ -1876,6 +1877,8 @@
"react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.2.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q=="], "react-native-is-edge-to-edge": ["react-native-is-edge-to-edge@1.2.1", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q=="],
"react-native-keyboard-controller": ["react-native-keyboard-controller@1.18.6", "", { "dependencies": { "react-native-is-edge-to-edge": "^1.2.1" }, "peerDependencies": { "react": "*", "react-native": "*", "react-native-reanimated": ">=3.0.0" } }, "sha512-K/RMw3MdtuykkACFN5d9RTapAcO0v4T34gmSyHkEraU5UsX+fxEHd6j4MvL7KUihvmLLod0NV/mQC0nL4cOurw=="],
"react-native-maps": ["react-native-maps@1.20.1", "", { "dependencies": { "@types/geojson": "^7946.0.13" }, "peerDependencies": { "react": ">= 17.0.1", "react-native": ">= 0.64.3", "react-native-web": ">= 0.11" }, "optionalPeers": ["react-native-web"] }, "sha512-NZI3B5Z6kxAb8gzb2Wxzu/+P2SlFIg1waHGIpQmazDSCRkNoHNY4g96g+xS0QPSaG/9xRBbDNnd2f2/OW6t6LQ=="], "react-native-maps": ["react-native-maps@1.20.1", "", { "dependencies": { "@types/geojson": "^7946.0.13" }, "peerDependencies": { "react": ">= 17.0.1", "react-native": ">= 0.64.3", "react-native-web": ">= 0.11" }, "optionalPeers": ["react-native-web"] }, "sha512-NZI3B5Z6kxAb8gzb2Wxzu/+P2SlFIg1waHGIpQmazDSCRkNoHNY4g96g+xS0QPSaG/9xRBbDNnd2f2/OW6t6LQ=="],
"react-native-otp-entry": ["react-native-otp-entry@1.8.5", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-TZNkIuUzZKAAWrC8X/A22ZHJdycLysxUNysrGf0yTmDLRUyf4zLXwVFcDYUcRNe763Hjaf5qvtKGILb6lDGzoA=="], "react-native-otp-entry": ["react-native-otp-entry@1.8.5", "", { "peerDependencies": { "react": "*", "react-native": "*" } }, "sha512-TZNkIuUzZKAAWrC8X/A22ZHJdycLysxUNysrGf0yTmDLRUyf4zLXwVFcDYUcRNe763Hjaf5qvtKGILb6lDGzoA=="],

View File

@@ -0,0 +1,92 @@
import { AccentColor, MainColor } from "@/constants/color-palet";
import { TEXT_SIZE_LARGE } from "@/constants/constans-value";
import React from "react";
import {
Keyboard,
StyleSheet,
TouchableWithoutFeedback,
View,
} from "react-native";
interface AlertCustomProps {
children: React.ReactNode;
isVisible: boolean;
}
export default function ModalCustom({
children,
isVisible,
}: AlertCustomProps) {
if (!isVisible) return null;
return (
<TouchableWithoutFeedback onPress={Keyboard.dismiss}>
<View style={styles.overlay}>
<View style={styles.alertBox}>
<View style={{ width: "100%" }}>{children}</View>
</View>
</View>
</TouchableWithoutFeedback>
);
}
const styles = StyleSheet.create({
overlay: {
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(0,0,0,0.5)",
justifyContent: "center",
alignItems: "center",
zIndex: 999,
paddingVertical: 20,
},
alertBox: {
width: "90%",
backgroundColor: MainColor.darkblue,
borderColor: AccentColor.blue,
borderWidth: 1,
borderRadius: 10,
padding: 20,
alignItems: "center",
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
elevation: 5,
},
alertTitle: {
fontSize: TEXT_SIZE_LARGE,
fontWeight: "bold",
marginBottom: 20,
color: MainColor.white_gray,
},
alertMessage: {
textAlign: "center",
marginBottom: 20,
color: MainColor.white_gray,
},
alertButtons: {
flexDirection: "row",
justifyContent: "space-between",
width: "100%",
},
alertButton: {
flex: 1,
padding: 10,
borderRadius: 50,
marginHorizontal: 5,
alignItems: "center",
},
leftButton: {
backgroundColor: "gray",
},
rightButton: {
backgroundColor: MainColor.green,
},
buttonText: {
color: "white",
fontWeight: "bold",
},
});

View File

@@ -1,3 +1,4 @@
import { AccentColor } from "@/constants/color-palet";
import Divider from "../Divider/Divider"; import Divider from "../Divider/Divider";
import Grid from "../Grid/GridCustom"; import Grid from "../Grid/GridCustom";
import AvatarComp from "../Image/AvatarComp"; import AvatarComp from "../Image/AvatarComp";
@@ -39,7 +40,7 @@ const AvatarUsernameAndOtherComponent = ({
</Grid.Col> </Grid.Col>
)} )}
</Grid> </Grid>
{withBottomLine && <Divider marginTop={0} />} {withBottomLine && <Divider color={AccentColor.blue} marginTop={0} />}
</> </>
); );
}; };

20
constants/api-storage.ts Normal file
View File

@@ -0,0 +1,20 @@
const API_IMAGE = {
/**
*
* @param fileId | file id from wibu storage , atau bisa disimpan di DB
* @param size | file size 10 - 1000 , tergantung ukuran file dan kebutuhan saar di tampilkan
* @type {string}
*/
GET: ({ fileId, size }: { fileId: string; size?: string }) =>
size
? `https://wibu-storage.wibudev.com/api/files/${fileId}-size-${size}`
: `https://wibu-storage.wibudev.com/api/files/${fileId}`,
/**
* @type {string}
* @returns alamat API dari wibu storage
*/
GET_NO_PARAMS: "https://wibu-storage.wibudev.com/api/files/",
};
export default API_IMAGE;

View File

@@ -1,71 +1,73 @@
export {listDataNotPublishInvesment, listDataPublishInvesment}; import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
const listDataNotPublishInvesment = [ export { listDataNotPublishInvesment, listDataPublishInvesment };
const listDataNotPublishInvesment = ({ data }: { data: any }) => [
{ {
label: "Target Dana", label: "Target Dana",
value: "Rp. 7.500.000", value: `Rp. ${formatCurrencyDisplay(data?.targetDana) || "-"}`,
}, },
{ {
label: "Harga Per Lembar", label: "Harga Per Lembar",
value: "Rp. 2.400", value: `Rp. ${formatCurrencyDisplay(data?.hargaLembar) || "-"}`,
}, },
{ {
label: "Return Of Investment (ROI)", label: "Return Of Investment (ROI)",
value: "3 %", value: `${data?.roi || "-"} %`,
}, },
{ {
label: "Total Lembar", label: "Total Lembar",
value: "1.200", value: data?.totalLembar || "-",
},
{
label: "Jadwal Pembagian",
value: "Rp. 2.880.000",
},
{
label: "Pembagian Deviden",
value: "Selamanya",
}, },
{ {
label: "Pencarian Investor", label: "Pencarian Investor",
value: "30 Hari", value: data && data?.MasterPencarianInvestor?.name + " hari" || "-",
},
{
label: "Jadwal Pembagian",
value: data && data?.MasterPembagianDeviden?.name + " bulan" || "-",
},
{
label: "Pembagian Deviden",
value: data?.MasterPeriodeDeviden?.name || "-",
}, },
]; ];
const listDataPublishInvesment = [ const listDataPublishInvesment = ({ data }: { data: any }) => [
{ {
label: "Investor", label: "Investor",
value: "10", value: data?.investor,
}, },
{ {
label: "Target Dana", label: "Target Dana",
value: "Rp. 7.500.000", value: data?.targetDana,
}, },
{ {
label: "Harga Per Lembar", label: "Harga Per Lembar",
value: "Rp. 2.400", value: data?.hargaPerLembar,
}, },
{ {
label: "Return Of Investment (ROI)", label: "Return Of Investment (ROI)",
value: "3 %", value: data?.roi + " %",
}, },
{ {
label: "Total Lembar", label: "Total Lembar",
value: "1.200", value: data?.totalLembar,
}, },
{ {
label: "Sisa Lembar", label: "Sisa Lembar",
value: "600", value: data?.sisaLembar,
}, },
{ {
label: "Jadwal Pembagian", label: "Jadwal Pembagian",
value: "Rp. 2.880.000", value: data?.jadwalPembagian,
}, },
{ {
label: "Pembagian Deviden", label: "Pembagian Deviden",
value: "Selamanya", value: data?.pembagianDeviden,
}, },
{ {
label: "Pencarian Investor", label: "Pencarian Investor",
value: "30 Hari", value: data?.pencarianInvestor,
}, },
]; ];

View File

@@ -49,6 +49,7 @@
"react-native-dotenv": "^3.4.11", "react-native-dotenv": "^3.4.11",
"react-native-gesture-handler": "~2.28.0", "react-native-gesture-handler": "~2.28.0",
"react-native-international-phone-number": "^0.9.3", "react-native-international-phone-number": "^0.9.3",
"react-native-keyboard-controller": "^1.18.6",
"react-native-maps": "1.20.1", "react-native-maps": "1.20.1",
"react-native-otp-entry": "^1.8.5", "react-native-otp-entry": "^1.8.5",
"react-native-pager-view": "6.9.1", "react-native-pager-view": "6.9.1",

View File

@@ -1,22 +1,33 @@
import { import {
AvatarUsernameAndOtherComponent, AvatarUsernameAndOtherComponent,
BoxWithHeaderSection, BoxWithHeaderSection,
Grid, Grid,
StackCustom, Spacing,
TextCustom StackCustom,
TextCustom,
} from "@/components"; } from "@/components";
export default function Collaboration_BoxDetailSection({ id }: { id: string }) { export default function Collaboration_BoxDetailSection({
data,
}: {
data: any;
}) {
return ( return (
<> <>
<BoxWithHeaderSection> <BoxWithHeaderSection>
<AvatarUsernameAndOtherComponent
avatar={data?.Author?.Profile?.imageId}
name={data?.Author?.username}
avatarHref={`/profile/${data?.Author?.Profile?.id}`}
withBottomLine
/>
<Spacing height={10}/>
<StackCustom> <StackCustom>
<AvatarUsernameAndOtherComponent />
<TextCustom align="center" bold size="large"> <TextCustom align="center" bold size="large">
Judul Proyek {id} {data?.title || ""}
</TextCustom> </TextCustom>
{listData.map((item, index) => ( {listData(data).map((item, index) => (
<Grid key={index}> <Grid key={index}>
<Grid.Col span={4}> <Grid.Col span={4}>
<TextCustom bold>{item.title}</TextCustom> <TextCustom bold>{item.title}</TextCustom>
@@ -32,23 +43,21 @@ export default function Collaboration_BoxDetailSection({ id }: { id: string }) {
); );
} }
const listData = [ const listData = (data: any) => [
{ {
title: "Industri", title: "Industri",
value: "Pilihan Industri", value: data?.ProjectCollaborationMaster_Industri?.name || "-",
}, },
{ {
title: "Deskripsi", title: "Lokasi",
value: "Deskripsi Proyek", value: data?.lokasi || "-",
}, },
{ {
title: "Tujuan Proyek", title: "Tujuan Proyek",
value: value: data?.purpose || "-",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
}, },
{ {
title: "Keuntungan Proyek", title: "Keuntungan Proyek",
value: value: data?.benefit || "-",
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
}, },
]; ];

View File

@@ -7,24 +7,12 @@ import {
import { Href } from "expo-router"; import { Href } from "expo-router";
function Collaboration_BoxPublishSection({ function Collaboration_BoxPublishSection({
id,
title,
username,
description,
href, href,
data,
// Avatar
sourceAvatar,
rightComponentAvatar, rightComponentAvatar,
}: { }: {
id: string;
title?: string;
username?: string;
description?: string;
href: Href; href: Href;
data: any;
// Avatar
sourceAvatar?: string;
rightComponentAvatar?: React.ReactNode; rightComponentAvatar?: React.ReactNode;
}) { }) {
return ( return (
@@ -32,21 +20,18 @@ function Collaboration_BoxPublishSection({
<BoxWithHeaderSection href={href}> <BoxWithHeaderSection href={href}>
<StackCustom gap={0}> <StackCustom gap={0}>
<AvatarUsernameAndOtherComponent <AvatarUsernameAndOtherComponent
avatarHref={`/profile/${id}`} avatarHref={`/profile/${data?.Author?.id}`}
name={username || "Username"} name={data?.Author?.username || "Username"}
rightComponent={rightComponentAvatar} rightComponent={rightComponentAvatar}
avatar={sourceAvatar as any} avatar={data?.Author?.Profile?.imageId}
withBottomLine withBottomLine
/> />
<StackCustom> <StackCustom>
<TextCustom truncate={2} size="large" bold align="center"> <TextCustom truncate size="large" bold align="center">
{title || "Lorem ipsum dolor sit"} {data?.title || "-"}
</TextCustom>
<TextCustom truncate={2}>
{description ||
"Lorem ipsum dolor sit amet consectetur adipisicing elit. Porro sed doloremque tempora soluta. Dolorem ex quidem ipsum tempora, ipsa, obcaecati quia suscipit numquam, voluptates commodi porro impedit natus quos doloremque!"}
</TextCustom> </TextCustom>
<TextCustom truncate={2}>{data?.purpose || "-"}</TextCustom>
{/* <TextCustom bold size="small" > {/* <TextCustom bold size="small" >
2 Partisipan 2 Partisipan
</TextCustom> */} </TextCustom> */}

View File

@@ -0,0 +1,222 @@
// ChatScreen.tsx
import React, { useEffect, useRef, useState } from "react";
import {
Dimensions,
Keyboard,
Platform,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import {
SafeAreaView
} from "react-native-safe-area-context";
type Message = {
id: string;
text: string;
sender: "me" | "other";
timestamp: Date;
};
const { height: SCREEN_HEIGHT } = Dimensions.get("window");
const ChatScreen: React.FC = () => {
const [messages, setMessages] = useState<Message[]>([
{
id: "1",
text: "Hai!",
sender: "other",
timestamp: new Date(Date.now() - 300000),
},
{
id: "2",
text: "Halo juga!",
sender: "me",
timestamp: new Date(Date.now() - 240000),
},
{
id: "3",
text: "Apa kabar?",
sender: "other",
timestamp: new Date(Date.now() - 180000),
},
]);
const [inputText, setInputText] = useState<string>("");
const [keyboardHeight, setKeyboardHeight] = useState<number>(0);
const scrollViewRef = useRef<ScrollView>(null);
useEffect(() => {
const show = Keyboard.addListener("keyboardDidShow", (e) => {
let kbHeight = e.endCoordinates.height;
// Di Android dengan edge-to-edge, kadang tinggi termasuk navigation bar
if (Platform.OS === "android") {
// Batasi maksimal 60% layar
kbHeight = Math.min(kbHeight, SCREEN_HEIGHT * 2);
}
setKeyboardHeight(kbHeight);
});
const hide = Keyboard.addListener("keyboardDidHide", () => {
setKeyboardHeight(0);
});
return () => {
show.remove();
hide.remove();
};
}, []);
useEffect(() => {
// Scroll ke bawah setelah pesan baru atau keyboard muncul
const timer = setTimeout(() => {
scrollViewRef.current?.scrollToEnd({ animated: true });
}, 100); // delay kecil untuk pastikan layout stabil
return () => clearTimeout(timer);
}, [messages, keyboardHeight]);
const handleSend = () => {
if (!inputText.trim()) return;
setMessages((prev) => [
...prev,
{
id: Date.now().toString(),
text: inputText.trim(),
sender: "me",
timestamp: new Date(),
},
]);
setInputText("");
};
return (
<SafeAreaView style={styles.safeArea}>
{/* Kontainer utama dengan padding bottom = tinggi keyboard */}
<View style={[styles.container, { paddingBottom: keyboardHeight }]}>
<ScrollView
ref={scrollViewRef}
style={styles.messagesContainer}
contentContainerStyle={styles.messagesContent}
keyboardShouldPersistTaps="handled"
>
{messages.map((msg) => (
<View
key={msg.id}
style={[
styles.messageBubble,
msg.sender === "me" ? styles.myMessage : styles.otherMessage,
]}
>
<Text style={styles.messageText}>{msg.text}</Text>
<Text style={styles.timestamp}>
{msg.timestamp.toLocaleTimeString([], {
hour: "2-digit",
minute: "2-digit",
})}
</Text>
</View>
))}
</ScrollView>
<View style={[styles.inputContainer, { paddingBottom: 10 }]}>
<TextInput
value={inputText}
onChangeText={setInputText}
placeholder="Ketik pesan..."
style={styles.textInput}
multiline
blurOnSubmit={false}
scrollEnabled={Platform.OS === "ios"} // ✅ Aktifkan scroll hanya di iOS
textAlignVertical="top"
/>
<TouchableOpacity
style={styles.sendButton}
onPress={handleSend}
disabled={!inputText.trim()}
>
<Text style={styles.sendButtonText}>Kirim</Text>
</TouchableOpacity>
</View>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: "#e5ddd5",
},
container: {
flex: 1,
backgroundColor: "#e5ddd5",
},
messagesContainer: {
flex: 1,
paddingHorizontal: 10,
},
messagesContent: {
paddingBottom: 10,
},
messageBubble: {
maxWidth: "80%",
padding: 10,
marginVertical: 4,
borderRadius: 12,
},
myMessage: {
alignSelf: "flex-end",
backgroundColor: "#dcf8c6",
},
otherMessage: {
alignSelf: "flex-start",
backgroundColor: "#ffffff",
},
messageText: {
fontSize: 16,
color: "#000",
},
timestamp: {
fontSize: 10,
color: "#666",
textAlign: "right",
marginTop: 4,
},
inputContainer: {
flexDirection: "row",
alignItems: "flex-end",
paddingHorizontal: 10,
paddingTop: 8,
backgroundColor: "#f0f0f0",
borderTopWidth: 1,
borderTopColor: "#ddd",
},
textInput: {
flex: 1,
borderWidth: 1,
borderColor: "#ccc",
borderRadius: 20,
paddingHorizontal: 12,
paddingVertical: 8,
fontSize: 16,
backgroundColor: "#fff",
maxHeight: 100,
},
sendButton: {
marginLeft: 10,
backgroundColor: "#34b7f1",
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 20,
},
sendButtonText: {
color: "#fff",
fontWeight: "bold",
},
});
export default ChatScreen;

View File

@@ -0,0 +1,292 @@
/* eslint-disable react-hooks/exhaustive-deps */
// ChatScreen.tsx
import { LoaderCustom } from "@/components";
import { AccentColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth";
import {
apiCollaborationGroupMessage,
apiCollaborationGroupMessageCreate,
} from "@/service/api-client/api-collaboration";
import { formatChatTime } from "@/utils/formatChatTime";
import { AntDesign } from "@expo/vector-icons";
import dayjs from "dayjs";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
import React, { useCallback, useEffect, useRef, useState } from "react";
import {
Dimensions,
Keyboard,
Platform,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import { ActivityIndicator } from "react-native-paper";
import { SafeAreaView } from "react-native-safe-area-context";
type IMessage = {
id: string;
createdAt: Date;
isActive: boolean;
message: string;
isFile: boolean;
userId: string;
User: {
select: {
id: true;
username: true;
};
};
};
const { height: SCREEN_HEIGHT } = Dimensions.get("window");
const ChatScreen: React.FC<{ id: string }> = ({ id }) => {
const { user } = useAuth();
const [messages, setMessages] = useState<IMessage[] | null>(null);
const [loadingMessage, setLoadingMessage] = useState<boolean>(false);
const [inputText, setInputText] = useState<string>("");
const [keyboardHeight, setKeyboardHeight] = useState<number>(0);
const scrollViewRef = useRef<ScrollView>(null);
useFocusEffect(
useCallback(() => {
onLoadMessage();
}, [id])
);
const onLoadMessage = async () => {
try {
setLoadingMessage(true);
const response = await apiCollaborationGroupMessage({ id: id as string });
if (response.success) {
setMessages(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadingMessage(false);
}
};
useEffect(() => {
const show = Keyboard.addListener("keyboardDidShow", (e) => {
let kbHeight = e.endCoordinates.height;
// Di Android dengan edge-to-edge, kadang tinggi termasuk navigation bar
if (Platform.OS === "android") {
// Batasi maksimal 60% layar
kbHeight = Math.min(kbHeight, SCREEN_HEIGHT * 0.6);
}
setKeyboardHeight(kbHeight);
});
const hide = Keyboard.addListener("keyboardDidHide", () => {
setKeyboardHeight(0);
});
return () => {
show.remove();
hide.remove();
};
}, []);
useEffect(() => {
// Scroll ke bawah setelah pesan baru atau keyboard muncul
const timer = setTimeout(() => {
scrollViewRef.current?.scrollToEnd({ animated: true });
}, 100); // delay kecil untuk pastikan layout stabil
return () => clearTimeout(timer);
}, [messages, keyboardHeight]);
const handleSend = async () => {
if (!inputText.trim()) return;
const newData = {
userId: user?.id,
message: inputText.trim(),
};
try {
const response = await apiCollaborationGroupMessageCreate({
id: id as string,
data: newData,
});
if (response.success) {
setMessages((prev: IMessage | any) => [
...prev,
{
id: Date.now().toString(),
createdAt: new Date(),
isActive: true,
message: inputText.trim(),
isFile: false,
userId: user?.id,
User: {
username: user?.username,
},
},
]);
setInputText("");
}
} catch (error) {
console.log("[ERROR]", error);
}
};
return (
<SafeAreaView style={styles.safeArea} edges={["bottom"]}>
{/* Kontainer utama dengan padding bottom = tinggi keyboard */}
<View style={[styles.container, { paddingBottom: keyboardHeight }]}>
<ScrollView
ref={scrollViewRef}
style={styles.messagesContainer}
contentContainerStyle={styles.messagesContent}
keyboardShouldPersistTaps="handled"
>
{loadingMessage ? (
<ActivityIndicator color="black" size={"large"} />
) : _.isEmpty(messages) ? (
<Text style={styles.isEmptyMessage}>
Belum ada pesan
</Text>
) : (
messages?.map((item: any) => (
<View
key={item.id}
style={[
styles.messageBubble,
item.userId === user?.id
? styles.myMessage
: styles.otherMessage,
]}
>
<Text style={styles.name}>{item?.User?.username}</Text>
<Text style={styles.messageText}>{item.message}</Text>
<Text style={styles.timestamp}>
{formatChatTime(item.createdAt)}
</Text>
</View>
))
)}
</ScrollView>
<View style={[styles.inputContainer, { paddingBottom: 10 }]}>
<TextInput
value={inputText}
onChangeText={setInputText}
placeholder="Ketik pesan..."
style={styles.textInput}
multiline
blurOnSubmit={false}
scrollEnabled={Platform.OS === "ios"} // ✅ Aktifkan scroll hanya di iOS
textAlignVertical="top"
/>
<TouchableOpacity
activeOpacity={0.7}
style={styles.sendButton}
onPress={handleSend}
disabled={!inputText.trim()}
>
<AntDesign name="send" size={ICON_SIZE_SMALL} color="white"/>
{/* <Text style={styles.sendButtonText}>Kirim</Text> */}
</TouchableOpacity>
</View>
</View>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
safeArea: {
flex: 1,
backgroundColor: "#e5ddd5",
},
container: {
flex: 1,
backgroundColor: "#e5ddd5",
},
isEmptyMessage: {
alignSelf: "center",
color: "#666",
marginTop: 20,
},
messagesContainer: {
flex: 1,
paddingHorizontal: 10,
},
messagesContent: {
paddingBottom: 10,
},
messageBubble: {
maxWidth: "80%",
padding: 10,
marginVertical: 4,
borderRadius: 12,
},
name: {
fontSize: 12,
color: "#666",
marginBottom: 4,
},
myMessage: {
alignSelf: "flex-end",
backgroundColor: "#dcf8c6",
},
otherMessage: {
alignSelf: "flex-start",
backgroundColor: "#ffffff",
},
messageText: {
fontSize: 16,
color: "#000",
},
timestamp: {
fontSize: 10,
color: "#666",
textAlign: "right",
marginTop: 4,
},
inputContainer: {
flexDirection: "row",
alignItems: "flex-end",
paddingHorizontal: 10,
paddingTop: 8,
backgroundColor: "#f0f0f0",
borderTopWidth: 1,
borderTopColor: "#ddd",
},
textInput: {
flex: 1,
borderWidth: 1,
borderColor: "#ccc",
borderRadius: 20,
paddingHorizontal: 12,
paddingVertical: 8,
fontSize: 16,
backgroundColor: "#fff",
maxHeight: 100,
},
sendButton: {
marginLeft: 10,
backgroundColor: AccentColor.blue,
// paddingHorizontal: 16,
// paddingVertical: 10,
borderRadius: "100%",
width: 40,
height: 40,
justifyContent: "center",
alignContent: "center",
alignItems: "center",
},
sendButtonText: {
color: "#fff",
fontWeight: "bold",
},
});
export default ChatScreen;

View File

@@ -15,10 +15,12 @@ export default function Collaboration_ProjectMainSelectedSection({
selected, selected,
setSelected, setSelected,
setOpenDrawerParticipant, setOpenDrawerParticipant,
listData,
}: { }: {
selected: (string | number)[]; selected: (string | number)[];
setSelected: (value: (string | number)[]) => void; setSelected: (value: (string | number)[]) => void;
setOpenDrawerParticipant: (value: boolean) => void; setOpenDrawerParticipant: (value: boolean) => void;
listData: any[];
}) { }) {
return ( return (
<BaseBox style={{ height: 500 }}> <BaseBox style={{ height: 500 }}>
@@ -31,7 +33,7 @@ export default function Collaboration_ProjectMainSelectedSection({
</TextCustom> </TextCustom>
<CheckboxGroup value={selected} onChange={setSelected}> <CheckboxGroup value={selected} onChange={setSelected}>
{Array.from({ length: 5 }).map((_, index) => ( {listData?.map((item: any, index: any) => (
<View key={index}> <View key={index}>
<Grid key={index}> <Grid key={index}>
<Grid.Col <Grid.Col

View File

@@ -1,30 +1,44 @@
import { import {
AvatarCustom, AvatarComp,
BaseBox, BoxWithHeaderSection,
ClickableCustom, ClickableCustom,
Grid, Grid,
Spacing, Spacing,
TextCustom, TextCustom
} from "@/components"; } from "@/components";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value"; import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { GStyles } from "@/styles/global-styles";
import { formatChatTime } from "@/utils/formatChatTime";
import { Entypo } from "@expo/vector-icons"; import { Entypo } from "@expo/vector-icons";
import { View } from "react-native"; import { View } from "react-native";
export default function Forum_CommentarBoxSection({ export default function Forum_CommentarBoxSection({
data, data,
setOpenDrawer, onSetData,
}: { }: {
data: any; data: any;
setOpenDrawer: (value: boolean) => void; onSetData: ({
setCommentId,
setOpenDrawer,
setCommentAuthorId,
}: {
setCommentId: string;
setOpenDrawer: boolean;
setCommentAuthorId: string;
}) => void;
}) { }) {
return ( return (
<> <>
<BaseBox> <BoxWithHeaderSection>
<View> <View>
<Grid> <Grid>
<Grid.Col span={2}> <Grid.Col span={2}>
<AvatarCustom href={`/profile/${data.id}`} /> <AvatarComp
href={`/profile/${data?.Author?.Profile?.id}`}
fileId={data?.Author?.Profile?.imageId}
size="base"
/>
</Grid.Col> </Grid.Col>
<Grid.Col <Grid.Col
span={8} span={8}
@@ -32,7 +46,7 @@ export default function Forum_CommentarBoxSection({
justifyContent: "center", justifyContent: "center",
}} }}
> >
<TextCustom>{data.name}</TextCustom> <TextCustom>{data?.Author?.username}</TextCustom>
</Grid.Col> </Grid.Col>
<Grid.Col <Grid.Col
@@ -43,7 +57,11 @@ export default function Forum_CommentarBoxSection({
> >
<ClickableCustom <ClickableCustom
onPress={() => { onPress={() => {
setOpenDrawer(true); onSetData({
setCommentId: data?.id,
setOpenDrawer: true,
setCommentAuthorId: data?.Author?.id,
});
}} }}
style={{ style={{
alignItems: "flex-end", alignItems: "flex-end",
@@ -58,14 +76,18 @@ export default function Forum_CommentarBoxSection({
</Grid.Col> </Grid.Col>
</Grid> </Grid>
<TextCustom>{data.deskripsi}</TextCustom> <View style={GStyles.forumBox}>
<TextCustom>{data.komentar}</TextCustom>
</View>
<Spacing /> <Spacing height={10} />
<View style={{ alignItems: "flex-end" }}> <View style={{ alignItems: "flex-end" }}>
<TextCustom>{data.date}</TextCustom> <TextCustom size="small" color="gray">
{formatChatTime(data?.createdAt)}
</TextCustom>
</View> </View>
</View> </View>
</BaseBox> </BoxWithHeaderSection>
</> </>
); );
} }

View File

@@ -1,5 +1,5 @@
import { import {
AvatarCustom, AvatarComp,
BaseBox, BaseBox,
ClickableCustom, ClickableCustom,
Grid, Grid,
@@ -8,6 +8,8 @@ import {
} from "@/components"; } from "@/components";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value"; import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { GStyles } from "@/styles/global-styles";
import { formatChatTime } from "@/utils/formatChatTime";
import { Entypo, Ionicons } from "@expo/vector-icons"; import { Entypo, Ionicons } from "@expo/vector-icons";
import { Href, router } from "expo-router"; import { Href, router } from "expo-router";
import { View } from "react-native"; import { View } from "react-native";
@@ -15,28 +17,32 @@ import { View } from "react-native";
export default function Forum_BoxDetailSection({ export default function Forum_BoxDetailSection({
data, data,
isTruncate, isTruncate,
setOpenDrawer,
setStatus,
href, href,
isRightComponent = true,
onSetData,
}: { }: {
data: any; data: any;
isTruncate?: boolean; isTruncate?: boolean;
setOpenDrawer: (value: boolean) => void;
setStatus: (value: string) => void;
href?: Href; href?: Href;
isRightComponent?: boolean;
onSetData: ({
setDataId,
setStatus,
setOpenDrawer,
setAuthorId,
}: {
setDataId: string;
setStatus: string;
setOpenDrawer: boolean;
setAuthorId: string;
}) => void;
}) { }) {
const deskripsiView = ( const deskripsiView = (
<View <View style={GStyles.forumBox}>
style={{
backgroundColor: MainColor.soft_darkblue,
padding: 8,
borderRadius: 8,
}}
>
{isTruncate ? ( {isTruncate ? (
<TextCustom truncate={2}>{data.deskripsi}</TextCustom> <TextCustom truncate={2}>{data?.diskusi}</TextCustom>
) : ( ) : (
<TextCustom>{data.deskripsi}</TextCustom> <TextCustom>{data?.diskusi}</TextCustom>
)} )}
</View> </View>
); );
@@ -47,42 +53,53 @@ export default function Forum_BoxDetailSection({
<View> <View>
<Grid> <Grid>
<Grid.Col span={2}> <Grid.Col span={2}>
<AvatarCustom href={`/profile/${data.id}`} /> <AvatarComp
fileId={data?.Author?.Profile?.imageId}
href={`/forum/${data?.Author?.id}/forumku`}
size={"base"}
/>
</Grid.Col> </Grid.Col>
<Grid.Col span={8}> <Grid.Col span={6}>
<TextCustom>{data.name}</TextCustom> <TextCustom truncate>{data?.Author?.username}</TextCustom>
{data.status === "Open" ? ( {data?.ForumMaster_StatusPosting?.status === "Open" ? (
<TextCustom bold size="small" color="green"> <TextCustom bold size="small" color="green">
{data.status} {data?.ForumMaster_StatusPosting?.status}
</TextCustom> </TextCustom>
) : ( ) : (
<TextCustom bold size="small" color="red"> <TextCustom bold size="small" color="red">
{data.status} {data?.ForumMaster_StatusPosting?.status}
</TextCustom> </TextCustom>
)} )}
</Grid.Col> </Grid.Col>
<Grid.Col <Grid.Col
span={2} span={4}
style={{ style={{
justifyContent: "center", justifyContent: "flex-start",
alignItems: "flex-end",
}} }}
> >
<ClickableCustom {isRightComponent && (
onPress={() => { <ClickableCustom
setOpenDrawer(true); onPress={() => {
setStatus(data.status); onSetData({
}} setDataId: data?.id,
style={{ setStatus: data?.ForumMaster_StatusPosting?.status,
alignItems: "flex-end", setAuthorId: data?.Author?.id,
}} setOpenDrawer: true,
> });
<Entypo }}
name="dots-three-horizontal" style={{
color={MainColor.white} alignItems: "flex-end",
size={ICON_SIZE_SMALL} }}
/> >
</ClickableCustom> <Entypo
name="dots-three-horizontal"
color={MainColor.white}
size={ICON_SIZE_SMALL}
/>
</ClickableCustom>
)}
</Grid.Col> </Grid.Col>
</Grid> </Grid>
@@ -110,11 +127,13 @@ export default function Forum_BoxDetailSection({
size={ICON_SIZE_SMALL} size={ICON_SIZE_SMALL}
color={MainColor.white} color={MainColor.white}
/> />
<TextCustom>{data.jumlahBalas}</TextCustom> <TextCustom>{data?.count}</TextCustom>
</View> </View>
</Grid.Col> </Grid.Col>
<Grid.Col span={6} style={{ alignItems: "flex-end" }}> <Grid.Col span={6} style={{ alignItems: "flex-end" }}>
<TextCustom size="small"> {data.date}</TextCustom> <TextCustom truncate size="small" color="gray">
{formatChatTime(data?.createdAt)}
</TextCustom>
</Grid.Col> </Grid.Col>
</Grid> </Grid>
</View> </View>

View File

@@ -2,24 +2,20 @@ import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value"; import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { Feather, Ionicons } from "@expo/vector-icons"; import { Feather, Ionicons } from "@expo/vector-icons";
export { drawerItemsForumBeranda, drawerItemsForumComentar }; export {
drawerItemsForumBerandaForAuthor,
drawerItemsForumComentarForAuthor,
drawerItemsForumBerandaForNonAuthor,
drawerItemsForumComentarForNonAuthor,
};
const drawerItemsForumBeranda = ({ const drawerItemsForumBerandaForAuthor = ({
id, id,
status, status,
}: { }: {
id: string; id: string;
status: string; status: string;
}) => [ }) => [
{
icon: (
<Ionicons name="flag" size={ICON_SIZE_SMALL} color={MainColor.white} />
),
label: "Laporkan diskusi",
// color: MainColor.white,
path: `/forum/${id}/report-posting`,
},
{ {
icon: ( icon: (
<Feather name="edit" size={ICON_SIZE_SMALL} color={MainColor.white} /> <Feather name="edit" size={ICON_SIZE_SMALL} color={MainColor.white} />
@@ -49,15 +45,18 @@ const drawerItemsForumBeranda = ({
}, },
]; ];
const drawerItemsForumComentar = ({ id }: { id: string }) => [ const drawerItemsForumBerandaForNonAuthor = ({ id }: { id: string }) => [
{ {
icon: ( icon: (
<Ionicons name="flag" size={ICON_SIZE_SMALL} color={MainColor.white} /> <Ionicons name="flag" size={ICON_SIZE_SMALL} color={MainColor.white} />
), ),
label: "Laporkan", label: "Laporkan diskusi",
// color: MainColor.white, // color: MainColor.white,
path: `/forum/${id}/report-commentar`, path: `/forum/${id}/report-posting`,
}, },
];
const drawerItemsForumComentarForAuthor = ({ id }: { id: string }) => [
{ {
icon: ( icon: (
<Ionicons name="trash" size={ICON_SIZE_SMALL} color={MainColor.white} /> <Ionicons name="trash" size={ICON_SIZE_SMALL} color={MainColor.white} />
@@ -67,3 +66,14 @@ const drawerItemsForumComentar = ({ id }: { id: string }) => [
path: "", path: "",
}, },
]; ];
const drawerItemsForumComentarForNonAuthor = ({ id }: { id: string }) => [
{
icon: (
<Ionicons name="flag" size={ICON_SIZE_SMALL} color={MainColor.white} />
),
label: "Laporkan",
// color: MainColor.white,
path: `/forum/${id}/report-commentar`,
},
];

View File

@@ -1,26 +1,67 @@
import { IMenuDrawerItem } from "@/components/_Interface/types"; import { IMenuDrawerItem } from "@/components/_Interface/types";
import MenuDrawerDynamicGrid from "@/components/Drawer/MenuDrawerDynamicGird"; import MenuDrawerDynamicGrid from "@/components/Drawer/MenuDrawerDynamicGird";
import { router } from "expo-router"; import { router } from "expo-router";
import { drawerItemsForumBeranda } from "../ListPage"; import { AlertDefaultSystem } from "@/components";
import {
drawerItemsForumBerandaForAuthor,
drawerItemsForumBerandaForNonAuthor,
} from "../ListPage";
import { useAuth } from "@/hooks/use-auth";
import { apiForumDelete } from "@/service/api-client/api-forum";
import Toast from "react-native-toast-message";
export default function Forum_MenuDrawerBerandaSection({ export default function Forum_MenuDrawerBerandaSection({
id, id,
status, status,
setIsDrawerOpen, setIsDrawerOpen,
setShowDeleteAlert, authorId,
setShowAlertStatus, handlerUpdateStatus,
}: { }: {
id: string; id: string;
status: string; status: string;
setIsDrawerOpen: (value: boolean) => void; setIsDrawerOpen: (value: boolean) => void;
setShowDeleteAlert: (value: boolean) => void; authorId: string;
setShowAlertStatus: (value: boolean) => void; handlerUpdateStatus?: (value: string) => void;
}) { }) {
const { user } = useAuth();
const handlePress = (item: IMenuDrawerItem) => { const handlePress = (item: IMenuDrawerItem) => {
if (item.label === "Hapus") { if (item.label === "Hapus") {
setShowDeleteAlert(true); AlertDefaultSystem({
title: "Hapus diskusi",
message: "Apakah Anda yakin ingin menghapus diskusi ini?",
textLeft: "Batal",
textRight: "Hapus",
onPressRight: async () => {
try {
const response = await apiForumDelete({ id });
if (response.success) {
Toast.show({
type: "success",
text1: "Berhasil dihapus",
});
router.back();
} else {
Toast.show({
type: "error",
text1: response.message,
});
}
} catch (error) {
console.log("[ERROR]", error);
}
},
});
} else if (item.label === "Buka forum" || item.label === "Tutup forum") { } else if (item.label === "Buka forum" || item.label === "Tutup forum") {
setShowAlertStatus(true); 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");
},
});
} else { } else {
router.push(item.path as any); router.push(item.path as any);
} }
@@ -32,9 +73,13 @@ export default function Forum_MenuDrawerBerandaSection({
<> <>
{/* Menu Items */} {/* Menu Items */}
<MenuDrawerDynamicGrid <MenuDrawerDynamicGrid
data={drawerItemsForumBeranda({ id, status })} data={
authorId === user?.id
? drawerItemsForumBerandaForAuthor({ id, status })
: drawerItemsForumBerandaForNonAuthor({ id })
}
columns={4} // Ubah ke 2 jika ingin 2 kolom per baris columns={4} // Ubah ke 2 jika ingin 2 kolom per baris
onPressItem={handlePress} onPressItem={handlePress as any}
/> />
</> </>
); );

View File

@@ -1,19 +1,67 @@
import { MenuDrawerDynamicGrid } from "@/components"; import { AlertDefaultSystem, MenuDrawerDynamicGrid } from "@/components";
import { drawerItemsForumComentar } from "../ListPage"; import { useAuth } from "@/hooks/use-auth";
import { router } from "expo-router"; import { router } from "expo-router";
import {
drawerItemsForumComentarForAuthor,
drawerItemsForumComentarForNonAuthor,
} from "../ListPage";
import { apiForumDeleteComment } from "@/service/api-client/api-forum";
import Toast from "react-native-toast-message";
export default function Forum_MenuDrawerCommentar({ export default function Forum_MenuDrawerCommentar({
id, id,
setShowDeleteAlert,
setIsDrawerOpen, setIsDrawerOpen,
commentId,
commentAuthorId,
listComment,
setListComment,
countComment,
setCountComment,
}: { }: {
id: string; id: string;
setShowDeleteAlert: (value: boolean) => void;
setIsDrawerOpen: (value: boolean) => void; setIsDrawerOpen: (value: boolean) => void;
commentId: string;
commentAuthorId: string;
listComment: any;
setListComment: (value: any) => void;
countComment: number;
setCountComment: (value: number) => void;
}) { }) {
const { user } = useAuth();
const handlePress = (item: any) => { const handlePress = (item: any) => {
if (item.label === "Hapus") { if (item.label === "Hapus") {
setShowDeleteAlert(true); AlertDefaultSystem({
title: "Hapus",
message: "Apakah Anda yakin ingin menghapus komentar ini?",
textLeft: "Batal",
textRight: "Hapus",
onPressLeft: () => {},
onPressRight: async () => {
try {
const response = await apiForumDeleteComment({ id: commentId });
if (response.success) {
setListComment(
listComment.filter((item: any) => item.id !== commentId)
);
setCountComment(countComment - 1);
Toast.show({
type: "success",
text1: "Berhasil dihapus",
});
} else {
Toast.show({
type: "error",
text1: response.message,
});
}
} catch (error) {
console.log("[ERROR]", error);
}
},
});
} else { } else {
router.push(item.path as any); router.push(item.path as any);
} }
@@ -24,7 +72,11 @@ export default function Forum_MenuDrawerCommentar({
return ( return (
<> <>
<MenuDrawerDynamicGrid <MenuDrawerDynamicGrid
data={drawerItemsForumComentar({ id })} data={
commentAuthorId === user?.id
? drawerItemsForumComentarForAuthor({ id })
: drawerItemsForumComentarForNonAuthor({ id })
}
columns={4} columns={4}
onPressItem={handlePress} onPressItem={handlePress}
/> />

View File

@@ -4,21 +4,30 @@ import { listDummyReportForum } from "@/lib/dummy-data/forum/report-list";
import { useState } from "react"; import { useState } from "react";
import { View } from "react-native"; import { View } from "react-native";
export default function Forum_ReportListSection() { export default function Forum_ReportListSection({
const [value, setValue] = useState<any | number>(""); listMaster,
selectReport,
setSelectReport,
}: {
listMaster: any[] | null;
selectReport: string;
setSelectReport: (value: string) => void;
}) {
return ( return (
<> <>
<BaseBox> <BaseBox>
<StackCustom> <StackCustom>
<RadioGroup value={value} onChange={setValue}> <RadioGroup value={selectReport} onChange={(val) => {
{listDummyReportForum.map((e, i) => ( setSelectReport(val);
}}>
{listMaster?.map((e, i) => (
<View key={i}> <View key={i}>
<RadioCustom <RadioCustom
label={e.title} label={e.title}
// value={i} // value={i}
value={e.title} value={e.id}
/> />
<TextCustom>{e.desc}</TextCustom> <TextCustom>{e.deskripsi}</TextCustom>
</View> </View>
))} ))}
</RadioGroup> </RadioGroup>

View File

@@ -10,10 +10,12 @@ import { View } from "react-native";
export default function Invesment_BoxDetailDataSection({ export default function Invesment_BoxDetailDataSection({
title, title,
imageId,
data, data,
bottomSection, bottomSection,
}: { }: {
title?: string; title?: string;
imageId?: string;
data: any; data: any;
bottomSection?: React.ReactNode; bottomSection?: React.ReactNode;
}) { }) {
@@ -21,14 +23,14 @@ export default function Invesment_BoxDetailDataSection({
<> <>
<BaseBox paddingBottom={0}> <BaseBox paddingBottom={0}>
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
<DummyLandscapeImage /> <DummyLandscapeImage imageId={imageId} />
<Spacing /> <Spacing />
<TextCustom align="center" size="xlarge" bold> <TextCustom align="center" size="xlarge" bold>
{title || "Judul Investasi"} {title || "Judul Investasi"}
</TextCustom> </TextCustom>
<Spacing /> <Spacing />
{data.map((item: any, index: any) => ( {data?.map((item: any, index: any) => (
<Grid key={index}> <Grid key={index}>
<Grid.Col span={4}> <Grid.Col span={4}>
<TextCustom bold>{item.label}</TextCustom> <TextCustom bold>{item.label}</TextCustom>

View File

@@ -1,22 +1,54 @@
import { AlertDefaultSystem, ButtonCustom, Grid } from "@/components"; import { AlertDefaultSystem, ButtonCustom, Grid } from "@/components";
import {
apiInvestmentDelete,
apiInvestmentUpdateStatus,
} from "@/service/api-client/api-investment";
import { deleteImageService } from "@/service/upload-service";
import { router } from "expo-router"; import { router } from "expo-router";
import { useState } from "react";
import Toast from "react-native-toast-message";
export default function Investment_ButtonStatusSection({ export default function Investment_ButtonStatusSection({
id,
status, status,
buttonPublish buttonPublish,
}: { }: {
id: string;
status: string; status: string;
buttonPublish?: React.ReactNode; buttonPublish?: React.ReactNode;
}) { }) {
const [isLoading, setIsLoading] = useState(false);
const handleBatalkanReview = () => { const handleBatalkanReview = () => {
AlertDefaultSystem({ AlertDefaultSystem({
title: "Batalkan Review", title: "Batalkan Review",
message: "Apakah Anda yakin ingin batalkan review ini?", message: "Apakah Anda yakin ingin batalkan review ini?",
textLeft: "Batal", textLeft: "Batal",
textRight: "Ya", textRight: "Ya",
onPressRight: () => { onPressRight: async () => {
console.log("Hapus"); try {
router.back(); setIsLoading(true);
const response = await apiInvestmentUpdateStatus({
id: id as string,
status: "draft",
});
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
if (response.success) {
Toast.show({
type: "success",
text1: "Berhasil Batalkan Review",
});
router.back();
} else {
Toast.show({
type: "error",
text1: "Gagal Batalkan Review",
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
}, },
}); });
}; };
@@ -27,9 +59,31 @@ export default function Investment_ButtonStatusSection({
message: "Apakah Anda yakin ingin ajukan review ini?", message: "Apakah Anda yakin ingin ajukan review ini?",
textLeft: "Batal", textLeft: "Batal",
textRight: "Ya", textRight: "Ya",
onPressRight: () => { onPressRight: async () => {
console.log("Hapus"); try {
router.back(); setIsLoading(true);
const response = await apiInvestmentUpdateStatus({
id: id as string,
status: "review",
});
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
if (response.success) {
Toast.show({
type: "success",
text1: "Berhasil Ajukan Review",
});
router.back();
} else {
Toast.show({
type: "error",
text1: "Gagal Ajukan Review",
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
}, },
}); });
}; };
@@ -40,9 +94,31 @@ export default function Investment_ButtonStatusSection({
message: "Apakah Anda yakin ingin edit kembali ini?", message: "Apakah Anda yakin ingin edit kembali ini?",
textLeft: "Batal", textLeft: "Batal",
textRight: "Ya", textRight: "Ya",
onPressRight: () => { onPressRight: async () => {
console.log("Hapus"); try {
router.back(); setIsLoading(true);
const response = await apiInvestmentUpdateStatus({
id: id as string,
status: "draft",
});
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
if (response.success) {
Toast.show({
type: "success",
text1: "Berhasil Update Status",
});
router.back();
} else {
Toast.show({
type: "error",
text1: "Gagal Update Status",
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
}, },
}); });
}; };
@@ -53,9 +129,55 @@ export default function Investment_ButtonStatusSection({
message: "Apakah Anda yakin ingin menghapus data ini?", message: "Apakah Anda yakin ingin menghapus data ini?",
textLeft: "Batal", textLeft: "Batal",
textRight: "Hapus", textRight: "Hapus",
onPressRight: () => { onPressRight: async () => {
console.log("Hapus"); try {
router.back(); setIsLoading(true);
const response = await apiInvestmentDelete({
id: id as string,
});
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
if (response.success) {
const deleteImage = await deleteImageService({
id: response?.data?.imageId as string,
});
if (!deleteImage.success) {
Toast.show({
type: "error",
text1: "Gagal Hapus Data",
});
return;
}
const deleteFile = await deleteImageService({
id: response?.data?.prospektusFileId as string,
});
if (!deleteFile.success) {
Toast.show({
type: "error",
text1: "Gagal Hapus Data",
});
return;
}
Toast.show({
type: "success",
text1: "Berhasil Hapus Data",
});
router.back();
} else {
Toast.show({
type: "error",
text1: "Gagal Hapus Data",
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
}, },
}); });
}; };
@@ -64,6 +186,7 @@ export default function Investment_ButtonStatusSection({
return ( return (
<> <>
<ButtonCustom <ButtonCustom
isLoading={isLoading}
backgroundColor="red" backgroundColor="red"
textColor="white" textColor="white"
onPress={handleOpenDeleteAlert} onPress={handleOpenDeleteAlert}
@@ -76,13 +199,11 @@ export default function Investment_ButtonStatusSection({
switch (status) { switch (status) {
case "publish": case "publish":
return <> return <>{buttonPublish}</>;
{buttonPublish}
</>;
case "review": case "review":
return ( return (
<ButtonCustom onPress={handleBatalkanReview}> <ButtonCustom isLoading={isLoading} onPress={handleBatalkanReview}>
Batalkan Review Batalkan Review
</ButtonCustom> </ButtonCustom>
); );
@@ -92,7 +213,7 @@ export default function Investment_ButtonStatusSection({
<> <>
<Grid> <Grid>
<Grid.Col span={6} style={{ paddingRight: 10 }}> <Grid.Col span={6} style={{ paddingRight: 10 }}>
<ButtonCustom onPress={handleAjukanReview}> <ButtonCustom isLoading={isLoading} onPress={handleAjukanReview}>
Ajukan Review Ajukan Review
</ButtonCustom> </ButtonCustom>
</Grid.Col> </Grid.Col>
@@ -108,7 +229,7 @@ export default function Investment_ButtonStatusSection({
<> <>
<Grid> <Grid>
<Grid.Col span={6} style={{ paddingRight: 10 }}> <Grid.Col span={6} style={{ paddingRight: 10 }}>
<ButtonCustom onPress={handleEditKembali}> <ButtonCustom isLoading={isLoading} onPress={handleEditKembali}>
Edit Kembali Edit Kembali
</ButtonCustom> </ButtonCustom>
</Grid.Col> </Grid.Col>

View File

@@ -10,26 +10,32 @@ import Investment_ButtonStatusSection from "./ButtonStatusSection";
export default function Invesment_DetailDataPublishSection({ export default function Invesment_DetailDataPublishSection({
status, status,
data,
bottomSection, bottomSection,
buttonSection, buttonSection,
}: { }: {
status: string; status: string;
data: any;
bottomSection?: React.ReactNode; bottomSection?: React.ReactNode;
buttonSection?: React.ReactNode; buttonSection?: React.ReactNode;
}) { }) {
// console.log("[DATA DETAIL]", JSON.stringify(data, null, 2));
return ( return (
<> <>
<StackCustom gap={"sm"}> <StackCustom gap={"sm"}>
<Invesment_BoxProgressSection status={status as string} /> <Invesment_BoxProgressSection status={status as string} />
<Invesment_BoxDetailDataSection <Invesment_BoxDetailDataSection
title={data?.title}
imageId={data?.imageId}
data={ data={
status === "publish" status === "publish"
? listDataPublishInvesment ? listDataPublishInvesment({ data })
: listDataNotPublishInvesment : listDataNotPublishInvesment({ data })
} }
bottomSection={bottomSection} bottomSection={bottomSection}
/> />
<Investment_ButtonStatusSection <Investment_ButtonStatusSection
id={data?.id}
status={status as string} status={status as string}
buttonPublish={buttonSection} buttonPublish={buttonSection}
/> />

View File

@@ -1,44 +1,43 @@
import { BaseBox, Grid, Spacing, TextCustom } from "@/components"; import { BaseBox, Grid, Spacing, TextCustom } from "@/components";
import API_IMAGE from "@/constants/api-storage";
import DUMMY_IMAGE from "@/constants/dummy-image-value"; import DUMMY_IMAGE from "@/constants/dummy-image-value";
import { formatCurrencyDisplay } from "@/utils/formatCurrencyDisplay";
import { Image } from "expo-image"; import { Image } from "expo-image";
import { Href } from "expo-router"; import { Href } from "expo-router";
import { View } from "react-native"; import { View } from "react-native";
interface Investment_StatusBoxProps { interface Investment_StatusBoxProps {
id: string; data: any;
status: string; status: string;
href?: Href href?: Href;
} }
export default function Investment_StatusBox({ export default function Investment_StatusBox({
id, data,
status, status,
href href,
}: Investment_StatusBoxProps) { }: Investment_StatusBoxProps) {
return ( return (
<BaseBox paddingTop={7} paddingBottom={7} href={href}> <BaseBox paddingTop={7} paddingBottom={7} href={href}>
<Grid> <Grid>
<Grid.Col span={6}> <Grid.Col span={6}>
<TextCustom truncate={2}> <TextCustom truncate={2}>{data?.title || ""}</TextCustom>
Title here : {status} Lorem ipsum dolor sit amet consectetur
adipisicing elit. Omnis, exercitationem, sequi enim quod distinctio
maiores laudantium amet, quidem atque repellat sit vitae qui aliquam
est veritatis laborum eum voluptatum totam!
</TextCustom>
<Spacing /> <Spacing />
<TextCustom bold size="small"> <TextCustom bold size="small">
Target Dana: Target Dana:
</TextCustom> </TextCustom>
<TextCustom>Rp. 7.500.000</TextCustom> <TextCustom truncate>
Rp. {formatCurrencyDisplay(data?.targetDana) || ""}
</TextCustom>
</Grid.Col> </Grid.Col>
<Grid.Col span={1}> <Grid.Col span={1}>
<View /> <View />
</Grid.Col> </Grid.Col>
<Grid.Col span={5}> <Grid.Col span={5}>
<Image <Image
source={DUMMY_IMAGE.background} source={API_IMAGE.GET({ fileId: data?.imageId })}
style={{ width: "auto", height: 100, borderRadius: 10 }} style={{ width: "auto", height: 100, borderRadius: 10 }}
/> />
</Grid.Col> </Grid.Col>

View File

@@ -1,5 +1,9 @@
import { AlertDefaultSystem, ButtonCustom, Grid } from "@/components"; import { AlertDefaultSystem, ButtonCustom, Grid } from "@/components";
import { apiJobDelete, apiJobUpdateStatus } from "@/service/api-client/api-job"; import {
apiJobDelete,
apiJobUpdateData,
apiJobUpdateStatus,
} from "@/service/api-client/api-job";
import { router } from "expo-router"; import { router } from "expo-router";
import Toast from "react-native-toast-message"; import Toast from "react-native-toast-message";
@@ -8,11 +12,13 @@ export default function Job_ButtonStatusSection({
status, status,
isLoading, isLoading,
onSetLoading, onSetLoading,
isArchive,
}: { }: {
id: string; id: string;
status: string; status: string;
isLoading: boolean; isLoading: boolean;
onSetLoading: (value: boolean) => void; onSetLoading: (value: boolean) => void;
isArchive?: boolean;
}) { }) {
const handleBatalkanReview = () => { const handleBatalkanReview = () => {
AlertDefaultSystem({ AlertDefaultSystem({
@@ -27,7 +33,7 @@ export default function Job_ButtonStatusSection({
id: id, id: id,
status: "draft", status: "draft",
}); });
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
if (response.success) { if (response.success) {
Toast.show({ Toast.show({
type: "success", type: "success",
@@ -64,7 +70,7 @@ export default function Job_ButtonStatusSection({
id: id, id: id,
status: "review", status: "review",
}); });
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
if (response.success) { if (response.success) {
Toast.show({ Toast.show({
type: "success", type: "success",
@@ -101,7 +107,7 @@ export default function Job_ButtonStatusSection({
id: id, id: id,
status: "draft", status: "draft",
}); });
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
if (response.success) { if (response.success) {
Toast.show({ Toast.show({
type: "success", type: "success",
@@ -137,7 +143,7 @@ export default function Job_ButtonStatusSection({
const response = await apiJobDelete({ const response = await apiJobDelete({
id: id, id: id,
}); });
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
if (response.success) { if (response.success) {
Toast.show({ Toast.show({
type: "success", type: "success",
@@ -161,6 +167,45 @@ export default function Job_ButtonStatusSection({
}); });
}; };
const handleArchive = () => {
AlertDefaultSystem({
title: "Arsipkan",
message: "Apakah Anda yakin ingin mengarsipkan data ini?",
textLeft: "Batal",
textRight: "Arsipkan",
onPressRight: async () => {
try {
onSetLoading(true);
const response = await apiJobUpdateData({
id: id,
data: isArchive,
category: "archive",
});
if (response.success) {
Toast.show({
type: "success",
text1: response.message,
});
// router.back();
router.replace("/(application)/(user)/job/(tabs)/archive");
} else {
Toast.show({
type: "info",
text1: "Info",
text2: response.message,
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
onSetLoading(false);
}
},
});
};
const DeleteButton = () => { const DeleteButton = () => {
return ( return (
<> <>
@@ -181,9 +226,9 @@ export default function Job_ButtonStatusSection({
return ( return (
<> <>
<ButtonCustom <ButtonCustom
isLoading={isLoading}
onPress={() => { onPress={() => {
console.log("Arsipkan"); handleArchive();
router.replace("/(application)/(user)/job/(tabs)/archive");
}} }}
> >
Arsipkan Arsipkan

View File

@@ -9,23 +9,32 @@ import { GStyles } from "@/styles/global-styles";
import { Voting_ComponentDetailDataSection } from "./ComponentDetailDataSection"; import { Voting_ComponentDetailDataSection } from "./ComponentDetailDataSection";
export function Voting_BoxDetailContributionSection({ export function Voting_BoxDetailContributionSection({
data,
headerAvatar, headerAvatar,
nameChoice,
}: { }: {
data: any;
headerAvatar?: React.ReactNode; headerAvatar?: React.ReactNode;
nameChoice?: string;
}) { }) {
return ( return (
<> <>
<BoxWithHeaderSection> <BoxWithHeaderSection>
{headerAvatar ? headerAvatar : <Spacing />} {headerAvatar && (
<>
{headerAvatar}
<Spacing />
</>
)}
<StackCustom gap={"lg"}> <StackCustom gap={"lg"}>
<Voting_ComponentDetailDataSection /> <Voting_ComponentDetailDataSection data={data} />
<StackCustom gap={"xs"}> <StackCustom gap={"sm"}>
<TextCustom bold size="small" align="center"> <TextCustom bold size="small" align="center">
Pilihan Anda Pilihan Anda
</TextCustom> </TextCustom>
<BadgeCustom style={[GStyles.alignSelfCenter]}> <BadgeCustom variant="light" size="lg" style={[GStyles.alignSelfCenter]}>
Pilihan 1 {nameChoice || "-"}
</BadgeCustom> </BadgeCustom>
</StackCustom> </StackCustom>
</StackCustom> </StackCustom>

View File

@@ -6,7 +6,11 @@ import {
CircleContainer, CircleContainer,
} from "@/components"; } from "@/components";
export default function Voting_BoxDetailHasilVotingSection() { export default function Voting_BoxDetailHasilVotingSection({
listData,
}: {
listData: any[];
}) {
return ( return (
<> <>
<BaseBox> <BaseBox>
@@ -16,10 +20,12 @@ export default function Voting_BoxDetailHasilVotingSection() {
</TextCustom> </TextCustom>
<Grid> <Grid>
{Array.from({ length: 4 }).map((_, i) => ( {listData?.map((item: any, i: number) => (
<Grid.Col span={3} style={{ alignItems: "center" }} key={i}> <Grid.Col span={12 / listData?.length} style={{ alignItems: "center" }} key={i}>
<CircleContainer value={9 % (i + 4)} /> <StackCustom>
<TextCustom size="small">Pilihan {i + 1}</TextCustom> <CircleContainer value={item?.jumlah} />
<TextCustom align="center" size="small">{item?.value}</TextCustom>
</StackCustom>
</Grid.Col> </Grid.Col>
))} ))}
</Grid> </Grid>

View File

@@ -1,21 +1,36 @@
import { import {
BadgeCustom,
BoxWithHeaderSection, BoxWithHeaderSection,
Spacing, Spacing,
StackCustom StackCustom,
TextCustom
} from "@/components"; } from "@/components";
import { Voting_ComponentDetailDataSection } from "./ComponentDetailDataSection"; import { Voting_ComponentDetailDataSection } from "./ComponentDetailDataSection";
import { GStyles } from "@/styles/global-styles";
export function Voting_BoxDetailHistorySection({ export function Voting_BoxDetailHistorySection({
headerAvatar, headerAvatar,
data,
nameChoice,
}: { }: {
headerAvatar?: React.ReactNode; headerAvatar?: React.ReactNode;
data: any;
nameChoice: string;
}) { }) {
return ( return (
<> <>
<BoxWithHeaderSection> <BoxWithHeaderSection>
{headerAvatar ? headerAvatar : <Spacing />} {headerAvatar ? headerAvatar : <Spacing />}
<StackCustom> <StackCustom>
<Voting_ComponentDetailDataSection /> <Voting_ComponentDetailDataSection data={data} />
<StackCustom gap={"sm"}>
<TextCustom bold size="small" align="center">
Pilihan Anda
</TextCustom>
<BadgeCustom variant="light" size="lg" style={[GStyles.alignSelfCenter]}>
{nameChoice || "-"}
</BadgeCustom>
</StackCustom>
</StackCustom> </StackCustom>
</BoxWithHeaderSection> </BoxWithHeaderSection>
</> </>

View File

@@ -1,4 +1,6 @@
import { import {
AlertDefaultSystem,
BadgeCustom,
BoxWithHeaderSection, BoxWithHeaderSection,
ButtonCustom, ButtonCustom,
Spacing, Spacing,
@@ -6,40 +8,109 @@ import {
TextCustom TextCustom
} from "@/components"; } from "@/components";
import { RadioCustom, RadioGroup } from "@/components/Radio/RadioCustom"; import { RadioCustom, RadioGroup } from "@/components/Radio/RadioCustom";
import { apiVotingVote } from "@/service/api-client/api-voting";
import { today } from "@/utils/dateTimeView";
import { router } from "expo-router";
import { useState } from "react"; import { useState } from "react";
import { View } from "react-native"; import { View } from "react-native";
import { Voting_ComponentDetailDataSection } from "./ComponentDetailDataSection"; import { Voting_ComponentDetailDataSection } from "./ComponentDetailDataSection";
export function Voting_BoxDetailPublishSection({ export function Voting_BoxDetailPublishSection({
headerAvatar, headerAvatar,
data,
userId,
isContribution,
nameChoice,
}: { }: {
headerAvatar?: React.ReactNode; headerAvatar?: React.ReactNode;
data?: any;
userId: string;
isContribution?: boolean;
nameChoice?: string;
}) { }) {
const [value, setValue] = useState<any | number>(""); const [value, setValue] = useState<any | number>("");
const handlerSubmitVote = async () => {
const newData = {
chooseId: value,
userId: userId,
};
try {
const response = await apiVotingVote({
id: data?.id,
data: newData,
});
if (response.success) {
router.push(`/voting/${data?.id}/list-of-contributor`);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
return ( return (
<> <>
<BoxWithHeaderSection> <BoxWithHeaderSection>
{headerAvatar ? headerAvatar : <Spacing />} {headerAvatar && (
<>
{headerAvatar}
<Spacing />
</>
)}
<StackCustom gap={"lg"}> <StackCustom gap={"lg"}>
<Voting_ComponentDetailDataSection /> <Voting_ComponentDetailDataSection data={data} />
<View> {isContribution ? (
<TextCustom bold size="small"> <StackCustom gap={"sm"}>
Pilihan : <TextCustom align="center" size="small" bold>
</TextCustom> Pilihan Anda :
<RadioGroup value={value} onChange={setValue}> </TextCustom>
{Array.from({ length: 4 }).map((_, i) => ( <View style={{ alignSelf: "center" }}>
<View key={i}> <BadgeCustom variant="light" size="lg">
<RadioCustom {nameChoice || "-"}
label={`Pilihan ${i + 1}`} </BadgeCustom>
value={`Pilihan ${i + 1}`} </View>
/> </StackCustom>
</View> ) : (
))} <>
</RadioGroup> <StackCustom>
</View> <TextCustom bold size="small">
Pilihan :
</TextCustom>
<RadioGroup value={value} onChange={setValue}>
{data?.Voting_DaftarNamaVote?.map((item: any, i: number) => (
<View key={i}>
<RadioCustom
disabled={
today.getDate() < new Date(data?.awalVote).getDate()
}
label={item?.value}
value={item?.id}
/>
</View>
))}
</RadioGroup>
</StackCustom>
<ButtonCustom onPress={() => console.log("vote")}>Vote</ButtonCustom> <ButtonCustom
disabled={value === ""}
onPress={() => {
AlertDefaultSystem({
title: "Anda melaukan voting",
message: "Yakin dengan pilihan anda ini ?",
textLeft: "Batal",
textRight: "Ya",
onPressRight: () => handlerSubmitVote(),
});
}}
>
Vote
</ButtonCustom>
</>
)}
</StackCustom> </StackCustom>
</BoxWithHeaderSection> </BoxWithHeaderSection>
</> </>

View File

@@ -2,35 +2,38 @@ import {
BoxWithHeaderSection, BoxWithHeaderSection,
Spacing, Spacing,
StackCustom, StackCustom,
TextCustom TextCustom,
} from "@/components"; } from "@/components";
import { View } from "react-native";
import { Voting_ComponentDetailDataSection } from "./ComponentDetailDataSection"; import { Voting_ComponentDetailDataSection } from "./ComponentDetailDataSection";
import { Ionicons } from "@expo/vector-icons";
export function Voting_BoxDetailSection({ export function Voting_BoxDetailSection({
headerAvatar, headerAvatar,
data,
}: { }: {
headerAvatar?: React.ReactNode; headerAvatar?: React.ReactNode;
data?: any;
}) { }) {
return ( return (
<> <>
<BoxWithHeaderSection> <BoxWithHeaderSection>
{headerAvatar ? headerAvatar : <Spacing />} {headerAvatar ? headerAvatar : <Spacing />}
<StackCustom> <StackCustom>
<Voting_ComponentDetailDataSection/> <Voting_ComponentDetailDataSection data={data} />
<Spacing/> <Spacing height={0} />
<View> <StackCustom>
<TextCustom bold size="small"> <TextCustom bold size="small">
Pilihan : Pilihan :
</TextCustom> </TextCustom>
{Array.from({ length: 3 }).map((_, i) => ( {data?.Voting_DaftarNamaVote?.map((item: any, i: number) => (
<View key={i}> <StackCustom key={i}>
<TextCustom>Nama Pilihan {i + 1}</TextCustom> <TextCustom>
<Spacing /> <Ionicons name="caret-forward" size={14} /> {item?.value}
</View> </TextCustom>
</StackCustom>
))} ))}
</View> </StackCustom>
</StackCustom> </StackCustom>
</BoxWithHeaderSection> </BoxWithHeaderSection>
</> </>

View File

@@ -15,37 +15,43 @@ export default function Voting_BoxPublishSection({
href, href,
id, id,
bottomComponent, bottomComponent,
data,
}: { }: {
href?: Href href?: Href;
id?: string id?: string;
bottomComponent?: React.ReactNode; bottomComponent?: React.ReactNode;
data?: any;
}) { }) {
return ( return (
<> <>
<BoxWithHeaderSection href={href}> <BoxWithHeaderSection href={href}>
<AvatarUsernameAndOtherComponent avatarHref="/profile/1" /> <AvatarUsernameAndOtherComponent
<Spacing /> avatar={data?.Author?.Profile?.imageId || ""}
<StackCustom> name={data?.Author?.username || "Username"}
avatarHref="/profile/1"
/>
<Spacing height={0} />
<StackCustom gap={"lg"}>
<TextCustom align="center" bold truncate size="large"> <TextCustom align="center" bold truncate size="large">
Voting Title {id} {data?.title || "-"}
</TextCustom> </TextCustom>
<BadgeCustom <BadgeCustom
style={{ width: "70%", alignSelf: "center" }} style={{ width: "70%", alignSelf: "center" }}
variant="light" variant="light"
> >
{dayjs().format("DD/MM/YYYY")} -{" "} {dayjs(data?.awalVote).format("DD/MM/YYYY")} -{" "}
{dayjs().add(1, "day").format("DD/MM/YYYY")} {dayjs(data?.akhirVote).format("DD/MM/YYYY")}
</BadgeCustom> </BadgeCustom>
<Grid> {/* <Grid>
{Array.from({ length: 4 }).map((_, i) => ( {Array.from({ length: 4 }).map((_, i) => (
<Grid.Col span={3} style={{ alignItems: "center" }} key={i}> <Grid.Col span={3} style={{ alignItems: "center" }} key={i}>
<CircleContainer value={9 % (i + 4)} /> <CircleContainer value={9 % (i + 4)} />
<TextCustom size="small">Pilihan {i + 1}</TextCustom> <TextCustom size="small">Pilihan {i + 1}</TextCustom>
</Grid.Col> </Grid.Col>
))} ))}
</Grid> </Grid> */}
{bottomComponent} {bottomComponent}
</StackCustom> </StackCustom>
</BoxWithHeaderSection> </BoxWithHeaderSection>

View File

@@ -1,11 +1,21 @@
import { AlertDefaultSystem, ButtonCustom, Grid } from "@/components"; import { AlertDefaultSystem, ButtonCustom, Grid } from "@/components";
import {
apiVotingDelete,
apiVotingUpdateStatus,
} from "@/service/api-client/api-voting";
import { router } from "expo-router"; import { router } from "expo-router";
import { View } from "react-native"; import Toast from "react-native-toast-message";
export default function Voting_ButtonStatusSection({ export default function Voting_ButtonStatusSection({
id,
status, status,
isLoading,
onSetLoading,
}: { }: {
id: string;
status: string; status: string;
isLoading: boolean;
onSetLoading: (value: boolean) => void;
}) { }) {
const handleBatalkanReview = () => { const handleBatalkanReview = () => {
AlertDefaultSystem({ AlertDefaultSystem({
@@ -13,9 +23,33 @@ export default function Voting_ButtonStatusSection({
message: "Apakah Anda yakin ingin batalkan review ini?", message: "Apakah Anda yakin ingin batalkan review ini?",
textLeft: "Batal", textLeft: "Batal",
textRight: "Ya", textRight: "Ya",
onPressRight: () => { onPressRight: async () => {
console.log("Hapus"); try {
router.back(); onSetLoading(true);
const response = await apiVotingUpdateStatus({
id: id as string,
status: "draft",
});
if (response?.success) {
Toast.show({
type: "success",
text1: response.message,
});
router.back();
} else {
Toast.show({
type: "info",
text1: "Info",
text2: response.message,
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
onSetLoading(false);
}
}, },
}); });
}; };
@@ -26,9 +60,33 @@ export default function Voting_ButtonStatusSection({
message: "Apakah Anda yakin ingin ajukan review ini?", message: "Apakah Anda yakin ingin ajukan review ini?",
textLeft: "Batal", textLeft: "Batal",
textRight: "Ya", textRight: "Ya",
onPressRight: () => { onPressRight: async () => {
console.log("Hapus"); try {
router.back(); onSetLoading(true);
const response = await apiVotingUpdateStatus({
id: id as string,
status: "review",
});
if (response?.success) {
Toast.show({
type: "success",
text1: response.message,
});
router.back();
} else {
Toast.show({
type: "info",
text1: "Info",
text2: response.message,
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
onSetLoading(false);
}
}, },
}); });
}; };
@@ -39,9 +97,33 @@ export default function Voting_ButtonStatusSection({
message: "Apakah Anda yakin ingin edit kembali ini?", message: "Apakah Anda yakin ingin edit kembali ini?",
textLeft: "Batal", textLeft: "Batal",
textRight: "Ya", textRight: "Ya",
onPressRight: () => { onPressRight: async () => {
console.log("Hapus"); try {
router.back(); onSetLoading(true);
const response = await apiVotingUpdateStatus({
id: id as string,
status: "draft",
});
if (response?.success) {
Toast.show({
type: "success",
text1: response.message,
});
router.back();
} else {
Toast.show({
type: "info",
text1: "Info",
text2: response.message,
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
onSetLoading(false);
}
}, },
}); });
}; };
@@ -52,9 +134,32 @@ export default function Voting_ButtonStatusSection({
message: "Apakah Anda yakin ingin menghapus data ini?", message: "Apakah Anda yakin ingin menghapus data ini?",
textLeft: "Batal", textLeft: "Batal",
textRight: "Hapus", textRight: "Hapus",
onPressRight: () => { onPressRight: async () => {
console.log("Hapus"); try {
router.back(); onSetLoading(true);
const response = await apiVotingDelete({
id: id as string,
});
if (response?.success) {
Toast.show({
type: "success",
text1: response.message,
});
router.back();
} else {
Toast.show({
type: "info",
text1: "Info",
text2: response.message,
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
onSetLoading(false);
}
}, },
}); });
}; };
@@ -63,6 +168,7 @@ export default function Voting_ButtonStatusSection({
return ( return (
<> <>
<ButtonCustom <ButtonCustom
isLoading={isLoading}
backgroundColor="red" backgroundColor="red"
textColor="white" textColor="white"
onPress={handleOpenDeleteAlert} onPress={handleOpenDeleteAlert}
@@ -79,7 +185,7 @@ export default function Voting_ButtonStatusSection({
case "review": case "review":
return ( return (
<ButtonCustom onPress={handleBatalkanReview}> <ButtonCustom isLoading={isLoading} onPress={handleBatalkanReview}>
Batalkan Review Batalkan Review
</ButtonCustom> </ButtonCustom>
); );
@@ -88,15 +194,14 @@ export default function Voting_ButtonStatusSection({
return ( return (
<> <>
<Grid> <Grid>
<Grid.Col span={5}> <Grid.Col span={6} style={{ paddingRight: 10 }}>
<ButtonCustom onPress={handleAjukanReview}> <ButtonCustom isLoading={isLoading} onPress={handleAjukanReview}>
Ajukan Review Ajukan Review
</ButtonCustom> </ButtonCustom>
</Grid.Col> </Grid.Col>
<Grid.Col span={2}> <Grid.Col span={6} style={{ paddingRight: 10 }}>
<View /> {DeleteButton()}
</Grid.Col> </Grid.Col>
<Grid.Col span={5}>{DeleteButton()}</Grid.Col>
</Grid> </Grid>
</> </>
); );
@@ -105,15 +210,14 @@ export default function Voting_ButtonStatusSection({
return ( return (
<> <>
<Grid> <Grid>
<Grid.Col span={5}> <Grid.Col span={6} style={{ paddingRight: 10 }}>
<ButtonCustom onPress={handleEditKembali}> <ButtonCustom isLoading={isLoading} onPress={handleEditKembali}>
Edit Kembali Edit Kembali
</ButtonCustom> </ButtonCustom>
</Grid.Col> </Grid.Col>
<Grid.Col span={2}> <Grid.Col span={6} style={{ paddingLeft: 10 }}>
<View /> {DeleteButton()}
</Grid.Col> </Grid.Col>
<Grid.Col span={5}>{DeleteButton()}</Grid.Col>
</Grid> </Grid>
</> </>
); );

View File

@@ -1,21 +1,15 @@
import { BadgeCustom, TextCustom } from "@/components"; import { BadgeCustom, StackCustom, TextCustom } from "@/components";
import { GStyles } from "@/styles/global-styles"; import { GStyles } from "@/styles/global-styles";
import dayjs from "dayjs"; import { dateTimeView } from "@/utils/dateTimeView";
import { View } from "react-native";
export function Voting_ComponentDetailDataSection() { export function Voting_ComponentDetailDataSection({ data }: { data?: any }) {
return ( return (
<> <>
<TextCustom align="center" bold size="large"> <TextCustom align="center" bold size="large">
Title of Voting Here {data?.title || "-"}
</TextCustom> </TextCustom>
<TextCustom> <TextCustom>{data?.deskripsi || "-"}</TextCustom>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Perspiciatis <StackCustom gap={"sm"}>
corporis blanditiis est provident corrupti facilis iste cum voluptate.
Natus eum aut quos consequatur doloribus fugiat sit ullam minima non
enim?
</TextCustom>
<View>
<TextCustom bold size="small" align="center"> <TextCustom bold size="small" align="center">
Batas Voting Batas Voting
</TextCustom> </TextCustom>
@@ -23,10 +17,13 @@ export function Voting_ComponentDetailDataSection() {
style={[GStyles.alignSelfCenter, { width: "70%" }]} style={[GStyles.alignSelfCenter, { width: "70%" }]}
variant="light" variant="light"
> >
{dayjs().format("DD/MM/YYYY")} -{" "} {data?.awalVote &&
{dayjs().add(1, "day").format("DD/MM/YYYY")} dateTimeView({ date: data?.awalVote, withoutTime: true })}{" "}
-{" "}
{data?.akhirVote &&
dateTimeView({ date: data?.akhirVote, withoutTime: true })}
</BadgeCustom> </BadgeCustom>
</View> </StackCustom>
</> </>
); );
} }

View File

@@ -0,0 +1,151 @@
import { apiConfig } from "../api-config";
export async function apiCollaborationCreate({ data }: { data: any }) {
try {
const response = await apiConfig.post(`/mobile/collaboration`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiCollaborationGetAll({
category,
authorId,
}: {
category: "beranda" | "participant" | "my-project" | "group";
authorId?: string;
}) {
try {
const authorQuery = authorId ? `&authorId=${authorId}` : "";
const response = await apiConfig.get(
`/mobile/collaboration?category=${category}${authorQuery}`
);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiCollaborationGetOne({ id }: { id: string }) {
try {
const response = await apiConfig.get(`/mobile/collaboration/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiCollaborationCreatePartisipasi({
id,
data,
}: {
id: string;
data: any;
}) {
try {
const response = await apiConfig.post(
`/mobile/collaboration/${id}/participants`,
{
data: data,
}
);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiCollaborationGetParticipants({
id,
category,
authorId,
}: {
id: string;
category: "list" | "check-participant";
authorId?: string;
}) {
try {
const authorQuery = authorId ? `&authorId=${authorId}` : "";
const response = await apiConfig.get(
`/mobile/collaboration/${id}/participants?category=${category}${authorQuery}`
);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiCollaborationCreateGroup({
id,
data,
}: {
id: string;
data: any;
}) {
try {
const response = await apiConfig.post(`/mobile/collaboration/${id}`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiCollaborationEditData({
id,
data,
}: {
id: string;
data: any;
}) {
try {
const response = await apiConfig.put(`/mobile/collaboration/${id}`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiCollaborationGroup({ id }: { id: string }) {
try {
const response = await apiConfig.get(`/mobile/collaboration/${id}/group`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiCollaborationGroupMessage({ id }: { id: string }) {
try {
const response = await apiConfig.get(`/mobile/collaboration/${id}/message`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiCollaborationGroupMessageCreate({
id,
data,
}: {
id: string;
data: any;
}) {
try {
const response = await apiConfig.post(
`/mobile/collaboration/${id}/message`,
{
data: data,
}
);
return response.data;
} catch (error) {
throw error;
}
}

View File

@@ -0,0 +1,112 @@
import { apiConfig } from "../api-config";
export async function apiForumCreate({ data }: { data: any }) {
try {
const response = await apiConfig.post(`/mobile/forum`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiForumGetAll({
search,
authorId,
}: {
search: string;
authorId?: string;
}) {
const authorQuery = authorId ? `?authorId=${authorId}` : "";
const searchQuery = search ? `?search=${search}` : "";
const query = search ? searchQuery : authorQuery;
try {
const response = await apiConfig.get(`/mobile/forum${query}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiForumGetOne({ id }: { id: string }) {
try {
const response = await apiConfig.get(`/mobile/forum/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiForumUpdate({ id, data }: { id: string; data: any }) {
try {
const response = await apiConfig.put(`/mobile/forum/${id}`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiForumUpdateStatus({
id,
data,
}: {
id: string;
data: any;
}) {
try {
const response = await apiConfig.post(`/mobile/forum/${id}`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiForumDelete({ id }: { id: string }) {
try {
const response = await apiConfig.delete(`/mobile/forum/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiForumCreateComment({
id,
data,
}: {
id: string;
data: any;
}) {
try {
const response = await apiConfig.post(`/mobile/forum/${id}/comment`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiForumGetComment({ id }: { id: string }) {
try {
const response = await apiConfig.get(`/mobile/forum/${id}/comment`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiForumDeleteComment({ id }: { id: string }) {
try {
const response = await apiConfig.delete(`/mobile/forum/${id}/comment`);
return response.data;
} catch (error) {
throw error;
}
}

View File

@@ -0,0 +1,80 @@
import { apiConfig } from "../api-config";
export async function apiInvestmentCreate({ data }: { data: any }) {
try {
const response = await apiConfig.post(`/mobile/investment`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiInvestmentGetByStatus({
authorId,
status,
}: {
authorId: string;
status: string;
}) {
try {
const response = await apiConfig.get(
`/mobile/investment/${authorId}/${status}`
);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiInvestmentGetById({ id }: { id: string }) {
try {
const response = await apiConfig.get(`/mobile/investment/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiInvestmentUpdateStatus({
id,
status,
}: {
id: string;
status: "publish" | "draft" | "review" | "reject";
}) {
console.log("[DATA FETCH]", JSON.stringify({ id, status }, null, 2));
try {
const response = await apiConfig.put(`/mobile/investment/${id}/${status}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiInvestmentDelete({ id }: { id: string }) {
try {
const response = await apiConfig.delete(`/mobile/investment/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiInvestmentUpdateData({
id,
data,
}: {
id: string;
data: any;
}) {
try {
const response = await apiConfig.put(`/mobile/investment/${id}`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}

View File

@@ -59,10 +59,24 @@ export async function apiJobDelete({ id }: { id: string }) {
} }
} }
export async function apiJobGetAll({ search }: { search?: string }) { export async function apiJobGetAll({
search,
category,
authorId,
}: {
search?: string;
category: "archive" | "beranda";
authorId?: string;
}) {
try { try {
const searchText = search ? `?search=${search}` : ""; let categoryText = category ? `?category=${category}` : "";
const response = await apiConfig.get(`/mobile/job${searchText}`); if (category === "archive") {
categoryText = `?category=${category}&authorId=${authorId}`;
}
const searchText = search ? `&search=${search}` : "";
const response = await apiConfig.get(
`/mobile/job${categoryText}${searchText}`
);
return response.data; return response.data;
} catch (error) { } catch (error) {
throw error; throw error;
@@ -72,12 +86,15 @@ export async function apiJobGetAll({ search }: { search?: string }) {
export async function apiJobUpdateData({ export async function apiJobUpdateData({
id, id,
data, data,
category,
}: { }: {
id: string; id: string;
data: any; data: any;
category: "edit" | "archive";
}) { }) {
try { try {
const response = await apiConfig.put(`/mobile/job/${id}`, { const categoryJob = category ? `?category=${category}` : "";
const response = await apiConfig.put(`/mobile/job/${id}${categoryJob}`, {
data: data, data: data,
}); });
return response.data; return response.data;

View File

@@ -28,4 +28,59 @@ export async function apiMasterEventType() {
} catch (error) { } catch (error) {
throw error; throw error;
} }
} }
export async function apiMasterCollaborationType() {
try {
const response = await apiConfig.get(
`/mobile/master/collaboration-industry`
);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiMasterForumReportList() {
try {
const response = await apiConfig.get(`/mobile/master/forum-report`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiForumCreateReportPosting({
id,
data,
}: {
id: string;
data: any;
}) {
try {
const response = await apiConfig.post(`/mobile/forum/${id}/report-posting`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiForumCreateReportCommentar({
id,
data,
}: {
id: string;
data: any;
}) {
try {
const response = await apiConfig.post(`/mobile/forum/${id}/report-commentar`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}

View File

@@ -0,0 +1,126 @@
import { apiConfig } from "../api-config";
export async function apiVotingCreate(data: any) {
try {
const response = await apiConfig.post(`/mobile/voting`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiVotingGetByStatus({
id,
status,
}: {
id: string;
status: string;
}) {
try {
const response = await apiConfig.get(`/mobile/voting/${id}/${status}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiVotingGetOne({ id }: { id: string }) {
try {
const response = await apiConfig.get(`/mobile/voting/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiVotingUpdateStatus({
id,
status,
}: {
id: string;
status: "draft" | "review" | "publish" | "reject";
}) {
try {
const response = await apiConfig.put(`/mobile/voting/${id}/${status}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiVotingDelete({ id }: { id: string }) {
try {
const response = await apiConfig.delete(`/mobile/voting/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiVotingUpdateData({
id,
data,
category,
}: {
id: string;
data: any;
category: "edit" | "archive";
}) {
const categoryQuery = `?category=${category}`;
try {
const response = await apiConfig.put(`/mobile/voting/${id}${categoryQuery}`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiVotingGetAll({ search, category, authorId }: { search?: string, category: "beranda" | "contribution" | "all-history" | "my-history", authorId?: string }) {
try {
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}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiVotingVote({ id, data }: { id: string; data: any }) {
try {
const response = await apiConfig.post(`/mobile/voting/${id}`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiVotingContribution({
id,
authorId,
category,
}: {
id: string;
authorId: string;
category: "list" | "checked";
}) {
const query =
category === "list"
? "?category=list"
: `?category=checked&authorId=${authorId}`;
try {
const response = await apiConfig.get(
`/mobile/voting/${id}/contribution${query}`
);
return response.data;
} catch (error) {
throw error;
}
}

View File

@@ -1,6 +1,7 @@
import AsyncStorage from "@react-native-async-storage/async-storage"; import AsyncStorage from "@react-native-async-storage/async-storage";
import axios, { AxiosInstance } from "axios"; import axios, { AxiosInstance } from "axios";
import Constants from "expo-constants"; import Constants from "expo-constants";
export const BASE_URL = Constants.expoConfig?.extra?.BASE_URL;
export const API_BASE_URL = Constants.expoConfig?.extra?.API_BASE_URL; export const API_BASE_URL = Constants.expoConfig?.extra?.API_BASE_URL;
export const apiConfig: AxiosInstance = axios.create({ export const apiConfig: AxiosInstance = axios.create({

View File

@@ -323,4 +323,10 @@ export const GStyles = StyleSheet.create({
alignSelfFlexEnd: { alignSelfFlexEnd: {
alignSelf: "flex-end", alignSelf: "flex-end",
}, },
forumBox: {
backgroundColor: MainColor.soft_darkblue,
borderRadius: 8,
paddingBlock: 20,
paddingInline: 10,
},
}); });

View File

@@ -1,5 +1,6 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
export const today = new Date();
export const dateTimeView = ({ export const dateTimeView = ({
date, date,
withoutTime = false, withoutTime = false,

35
utils/formatChatTime.ts Normal file
View File

@@ -0,0 +1,35 @@
// utils/formatChatTime.ts
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import 'dayjs/locale/id';
dayjs.extend(relativeTime);
dayjs.locale('id');
/**
* Format waktu pesan untuk tampilan chat
* @param date ISO string atau Date object
* @returns string formatted time
*/
export const formatChatTime = (date: string | Date): string => {
const messageDate = dayjs(date);
const now = dayjs();
// Jika hari ini
if (messageDate.isSame(now, 'day')) {
return messageDate.format('HH.mm'); // contoh: "14.30"
}
// Jika kemarin
if (messageDate.isSame(now.subtract(1, 'day'), 'day')) {
return messageDate.format('dddd HH:mm');
}
// Jika dalam 7 hari terakhir (tapi bukan kemarin/ hari ini)
if (now.diff(messageDate, 'day') < 7) {
return messageDate.format('dddd HH:mm'); // contoh: "Senin 14:30"
}
// Lebih dari seminggu lalu → tampilkan tanggal
return messageDate.format('D MMM YYYY'); // contoh: "12 Mei 2024"
};

View File

@@ -0,0 +1,46 @@
/**
* Memformat angka menjadi string dengan format mata uang lokal (misal: 3500000 → "3.500.000")
* Hanya untuk keperluan tampilan. Nilai asli tetap berupa number/string mentah.
*
* @param value - Angka yang akan diformat (bisa number atau string)
* @param locale - Lokal untuk format (default: 'id-ID' untuk format Indonesia)
* @param currency - Kode mata uang (opsional, default: tidak ditampilkan)
* @returns string yang sudah diformat tanpa simbol mata uang
*/
export const formatCurrencyDisplay = (
value: number | string | null | undefined,
locale: string = "id-ID",
currency?: string
): string => {
// Handle nilai null/undefined/empty
if (value === null || value === undefined || value === "") {
return "";
}
// Pastikan value adalah number
const numValue = typeof value === "string" ? parseFloat(value) : value;
// Jika parsing gagal, kembalikan string kosong
if (isNaN(numValue)) {
return "";
}
// Gunakan Intl.NumberFormat untuk format lokal
const formatter = new Intl.NumberFormat(locale, {
style: currency ? "currency" : "decimal",
currency: currency,
minimumFractionDigits: 0,
maximumFractionDigits: 0,
});
let formatted = formatter.format(numValue);
// Jika tidak ingin simbol mata uang, hapus simbolnya (misal: "Rp" atau "IDR")
if (!currency) {
// Hapus simbol non-digit/non-koma/non-titik (misal: "Rp", "IDR", "$", dll)
// Tapi pertahankan angka, koma, titik, dan spasi jika ada
formatted = formatted.replace(/[^\d.,\s]/g, "").trim();
}
return formatted;
};

117
utils/pickFile.ts Normal file
View File

@@ -0,0 +1,117 @@
import * as ImagePicker from "expo-image-picker";
import * as DocumentPicker from "expo-document-picker";
import { Alert } from "react-native";
const ALLOWED_IMAGE_EXTENSIONS = ["jpg", "jpeg", "png"];
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB
export interface IFileData {
uri: string;
name: string;
size: number;
}
export type AllowedFileType = "image" | "pdf" | undefined;
export interface PickFileOptions {
setImageUri?: (file: IFileData) => void;
setPdfUri?: (file: IFileData) => void;
allowedType?: AllowedFileType; // <-- Tambahkan prop ini
}
export default async function pickFile({
setImageUri,
setPdfUri,
allowedType,
}: PickFileOptions): Promise<void> {
if (allowedType === "image") {
await pickImage(setImageUri);
} else if (allowedType === "pdf") {
await pickPdf(setPdfUri);
} else {
// Mode fleksibel: tampilkan pilihan
Alert.alert(
"Pilih Jenis File",
"Pilih sumber file yang ingin diunggah:",
[
{ text: "Batal", style: "cancel" },
{ text: "Dokumen (PDF)", onPress: () => pickPdf(setPdfUri) },
{ text: "Gambar", onPress: () => pickImage(setImageUri) },
],
{ cancelable: true }
);
}
}
// --- Fungsi internal: pickImage ---
async function pickImage(setImageUri?: (file: IFileData) => void) {
const { status } = await ImagePicker.requestMediaLibraryPermissionsAsync();
if (status !== "granted") {
Alert.alert(
"Izin Ditolak",
"Izinkan akses ke galeri untuk memilih gambar."
);
return;
}
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
allowsEditing: true,
aspect: [4, 3],
quality: 1,
});
if (result.canceled || !result.assets?.[0]) return;
const asset = result.assets[0];
const uri = asset.uri;
const filename = uri.split("/").pop() || `image_${Date.now()}.jpg`;
const size = asset.fileSize ?? 0;
if (size > MAX_FILE_SIZE) {
Alert.alert("File Terlalu Besar", "Ukuran maksimal adalah 5MB.");
return;
}
const extMatch = /\.(\w+)$/.exec(filename.toLowerCase());
const extension = extMatch ? extMatch[1] : "jpg";
if (!ALLOWED_IMAGE_EXTENSIONS.includes(extension)) {
Alert.alert(
"Format Tidak Didukung",
"Hanya JPG, JPEG, dan PNG yang diperbolehkan."
);
return;
}
setImageUri?.({ uri, name: filename, size });
}
// --- Fungsi internal: pickPdf ---
async function pickPdf(setPdfUri?: (file: IFileData) => void) {
const result = await DocumentPicker.getDocumentAsync({
type: "application/pdf", // Hanya PDF
copyToCacheDirectory: true,
});
if (result.canceled || !result.assets?.[0]) return;
const asset = result.assets[0];
const { uri, name, size } = asset;
const filename = name || `document_${Date.now()}.pdf`;
const fileSize = size ?? 0;
if (fileSize > MAX_FILE_SIZE) {
Alert.alert("File Terlalu Besar", "Ukuran maksimal adalah 5MB.");
return;
}
// Validasi ekstensi (extra safety)
const extMatch = /\.(\w+)$/.exec(filename.toLowerCase());
if (extMatch?.[1] !== "pdf") {
Alert.alert("File Tidak Valid", "Hanya file PDF yang diperbolehkan.");
return;
}
setPdfUri?.({ uri, name: filename, size: fileSize });
}