Compare commits

..

22 Commits

Author SHA1 Message Date
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
60b0befa60 API Job
Add:
- api-client/api-job: kumpulan fetch api

Fix:
- UI beranda , status sudah terintergrasi dengan API
- UI detail status, detail utama sudah terintergrasi dengan API
- Search pada beranda sudah terintegrasi
- Edit sudah terintergrasi

### No Issue
2025-09-16 17:27:58 +08:00
3287f4c287 Event
Fix:
-  user)/event/(tabs)/contribution: tampilan creator yang di depan
- /Event/ButtonStatusSection: return message

### No Issuue
2025-09-16 11:17:36 +08:00
76fb14ed0c Event
Fix: Intergrasi pada UI
- Beranda, Kontibusi & Riwayat
- Detail beranda, detail kontribusi & detail riwayat
- List partisipan

### No issue
2025-09-15 17:17:50 +08:00
1d2153b253 Event
Fix: Intergrasi tampilan ke API

Package.json
Fix: Pem baharuan SDK53 -> SDK54

### No Issue
2025-09-12 15:57:30 +08:00
005b798688 Event
Fix:
- UI: status, detail status, delete button, detail utama, tampilan utama
- Semua terintergrasi ke API mobile

### No Issue
2025-09-12 14:14:37 +08:00
b6d4c0e6a6 API Event
Add:
- api-client/api-event : fetch get one, update data, update status, create event

Fix:
- UI : create , detail dan status: untuk menyambungkan ke API

### No Issue
2025-09-11 17:34:08 +08:00
3854db9330 Event
Fix:
- Create event
- api master tipe event

## NO Issue
2025-09-10 17:43:01 +08:00
fc181bda7c API
User search:
Fix:
-  api get all user
- searching by username

Portofolio:
Fix:
- dot button hanya muncul jika user yang memiliki portofolio tersebut yang melihat

Profile:
Fix:
- dot button muncul hanya untuk user yang memiliki akunnya

### No Issue
2025-09-10 16:33:39 +08:00
fb822d20b6 Portofolio
Fix: API edit detail data

### No Issue
2025-09-10 12:12:11 +08:00
0e708dde0f API Portofolio
Fix:
- edit select sub bidang

### NO Issue
2025-09-03 17:42:17 +08:00
9a915c55d2 Portofolio
Fix:
- Sub bidang bisnis

### No Issu
2025-09-02 18:29:28 +08:00
6887f85e6a Poortofoloio
Fix:
- service/api-client/api-portofolio.ts : api edit detail. logo, medsos

### No Issue
2025-09-01 17:10:25 +08:00
bb95e8ccbd API
Add:
- service/api-client/api-file.ts: upload & delete

Portofolio
Fix:
- user)/portofolio/[id]/create.tsx: Loading submit
- (user)/portofolio/[id]/index.tsx: Delete button recode

Profile
Fix:
- (user)/profile/[id]/update-photo && upload-backgroud: delete image yang kama

### No Issue
2025-09-01 12:11:21 +08:00
41a4a94255 Portofolio
Add:
- ervice/api-client/api-portofolio.ts
- creens/Portofolio/BoxPortofolioView.tsx
- screens/Portofolio/ButtonCreatePortofolio.tsx
- create dan show

### No Issue
2025-08-29 17:42:33 +08:00
88527d5bb6 Image : fix dummy user
### No Issue
2025-08-28 17:02:38 +08:00
40441c929f API Upload Image
Profile:
- (user)/profile/[id]/update-background.tsx : perbaikan api
-  app/(application)/(user)/profile/[id]/update-photo.tsx

Fix:
-   service/upload-service.ts : ganti Toast menjadi throw untuk dapatkna error

### No Issue
2025-08-28 15:18:15 +08:00
d3c4f04e07 Profile
Add:
- Api upload background
- /api-client/api-validation.ts

### No Issue
2025-08-27 17:41:42 +08:00
4fc2c90702 Profile
Add:
- Api background profile

Asset
Add:
- assets/images/loading.gif: untuk loading

### No Issue
2025-08-27 14:38:37 +08:00
2227aaa99f Profile
Fix:
- profile/[id]/edit.tsx: api upload
- profile/[id]/update-photo.tsx: api upload
- service/api-client/api-profile.ts: api profile bisa memilih kategori

Component
Add:
- components/Image/AvatarComp.tsx

### No Issue
2025-08-27 12:16:31 +08:00
105 changed files with 6254 additions and 19593 deletions

View File

@@ -36,6 +36,7 @@ export default {
plugins: [ plugins: [
'expo-router', 'expo-router',
'expo-web-browser',
[ [
'expo-splash-screen', 'expo-splash-screen',
{ {

View File

@@ -1,9 +1,33 @@
import { LandscapeFrameUploaded, ViewWrapper } from "@/components"; import { CenterCustom, TextCustom, ViewWrapper } from "@/components";
import API_STRORAGE from "@/constants/base-url-api-strorage";
import { Image } from "expo-image";
import { useLocalSearchParams } from "expo-router";
import React, { useState } from "react";
export default function PreviewImage() { export default function PreviewImage() {
const { id } = useLocalSearchParams();
const [isLoading, setIsLoading] = useState(true);
return ( return (
<ViewWrapper> <ViewWrapper>
<LandscapeFrameUploaded /> {id ? (
<Image
onLoad={() => {
setIsLoading(false);
}}
source={
isLoading
? require("@/assets/images/loading.gif")
: API_STRORAGE.GET({ fileId: id as string })
}
contentFit="contain"
style={{ width: "100%", height: "100%" }}
/>
) : (
<CenterCustom>
<TextCustom>File not found</TextCustom>
</CenterCustom>
)}
</ViewWrapper> </ViewWrapper>
); );
} }

View File

@@ -509,6 +509,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

@@ -8,7 +8,7 @@ import {
Spacing, Spacing,
StackCustom, StackCustom,
TextCustom, TextCustom,
ViewWrapper 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";

View File

@@ -1,43 +1,115 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
AvatarCustom,
AvatarUsernameAndOtherComponent, AvatarUsernameAndOtherComponent,
BoxWithHeaderSection, BoxWithHeaderSection,
Grid, LoaderCustom,
Spacing,
StackCustom, StackCustom,
TextCustom, TextCustom,
ViewWrapper ViewWrapper
} from "@/components"; } from "@/components";
import React from "react"; import { useAuth } from "@/hooks/use-auth";
import {
apiEventGetAll
} from "@/service/api-client/api-event";
import { dateTimeView } from "@/utils/dateTimeView";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
import React, { useCallback, useState } from "react";
export default function EventContribution() { export default function EventContribution() {
const { user } = useAuth();
const [listData, setListData] = useState<any>([]);
const [isLoadList, setIsLoadList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [user?.id])
);
async function onLoadData() {
try {
setIsLoadList(true);
const response = await apiEventGetAll({
category: "contribution",
userId: user?.id,
});
console.log("[DATA] ", JSON.stringify(response.data, null, 2));
if (response.success) {
setListData(response.data);
// const responseListParticipants = await apiEventListOfParticipants({
// id: response?.data?.Event?.id,
// });
// console.log(
// "[LIST PARTICIPANTS]",
// JSON.stringify(responseListParticipants.data, null, 2)
// );
// if (responseListParticipants.success) {
// setListParticipants(responseListParticipants.data);
// }
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadList(false);
}
}
return ( return (
<ViewWrapper hideFooter> <ViewWrapper hideFooter>
{Array.from({ length: 10 }).map((_, index) => ( {isLoadList ? (
<BoxWithHeaderSection key={index} href={`/event/${index}/contribution`}> <LoaderCustom />
<StackCustom> ) : _.isEmpty(listData) ? (
<AvatarUsernameAndOtherComponent <TextCustom align="center">Belum ada kontribusi</TextCustom>
avatarHref={`/profile/${index}`} ) : (
rightComponent={ listData.map((item: any, index: number) => (
<TextCustom truncate> <BoxWithHeaderSection
{new Date().toLocaleDateString()} key={index}
</TextCustom> href={`/event/${item?.Event?.id}/contribution`}
} >
/> <StackCustom>
<AvatarUsernameAndOtherComponent
avatar={item?.Event?.Author?.Profile?.imageId}
avatarHref={`/profile/${item?.Event?.Author?.Profile?.id}`}
name={item?.Event?.Author?.username}
rightComponent={
<TextCustom truncate>
{dateTimeView({
date: item?.Event?.tanggal,
withoutTime: true,
})}
</TextCustom>
}
/>
<TextCustom bold align="center" size="xlarge"> <TextCustom bold align="center" size="xlarge" truncate={2}>
Judul Event Disini {item?.Event?.title}
</TextCustom> </TextCustom>
<Spacing height={0} />
<Grid> {/* <Grid>
{Array.from({ length: 4 }).map((_, index2) => ( {item?.Event?.Event_Peserta?.map(
<Grid.Col span={3} key={index2}> (item2: any, index2: number) => (
<AvatarCustom size="sm" href={`/profile/${index2}`} /> <Grid.Col
</Grid.Col> style={{ alignItems: "center" }}
))} span={12 / item?.Event?.Event_Peserta?.length}
</Grid> key={index2}
</StackCustom> >
</BoxWithHeaderSection> <AvatarComp
))} size="base"
href={`/profile/${item2?.User?.Profile?.id}`}
fileId={item2?.User?.Profile?.imageId}
/>
</Grid.Col>
)
)}
</Grid> */}
</StackCustom>
</BoxWithHeaderSection>
))
)}
</ViewWrapper> </ViewWrapper>
); );
} }

View File

@@ -1,12 +1,41 @@
import { ButtonCustom, Spacing, TextCustom } 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 Event_BoxPublishSection from "@/screens/Event/BoxPublishSection"; import Event_BoxPublishSection from "@/screens/Event/BoxPublishSection";
import { useState } from "react"; import { apiEventGetAll } from "@/service/api-client/api-event";
import { dateTimeView } from "@/utils/dateTimeView";
import _ from "lodash";
import { useEffect, useState } from "react";
import { View } from "react-native"; import { View } from "react-native";
export default function EventHistory() { export default function EventHistory() {
const [activeCategory, setActiveCategory] = useState<string | null>("all"); const [activeCategory, setActiveCategory] = useState<string | null>("all");
const { user } = useAuth();
const [listData, setListData] = useState<any>([]);
const [isLoadList, setIsLoadList] = useState(false);
useEffect(() => {
onLoadData({ userId: user?.id });
}, [user?.id, activeCategory]);
async function onLoadData({ userId }: { userId?: string }) {
try {
setIsLoadList(true);
const response = await apiEventGetAll({
category: activeCategory === "all" ? "all-history" : "my-history",
userId: userId,
});
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadList(false);
}
}
const handlePress = (item: any) => { const handlePress = (item: any) => {
setActiveCategory(item); setActiveCategory(item);
@@ -52,17 +81,24 @@ export default function EventHistory() {
return ( return (
<ViewWrapper headerComponent={headerComponent} hideFooter> <ViewWrapper headerComponent={headerComponent} hideFooter>
{Array.from({ length: 10 }).map((_, index) => ( {isLoadList ? (
<Event_BoxPublishSection <LoaderCustom />
key={index.toString()} ) : _.isEmpty(listData) ? (
id={index.toString()} <TextCustom align="center">Belum ada riwayat</TextCustom>
username={`Riwayat ${activeCategory === "main" ? "Saya" : "Semua"}`} ) : (
rightComponentAvatar={ listData.map((item: any, index: number) => (
<TextCustom>{new Date().toLocaleDateString()}</TextCustom> <Event_BoxPublishSection
} key={index.toString()}
href={`/event/${index}/history`} data={item}
/> rightComponentAvatar={
))} <TextCustom>
{dateTimeView({ date: item?.tanggal, withoutTime: true })}
</TextCustom>
}
href={`/event/${item.id}/history`}
/>
))
)}
</ViewWrapper> </ViewWrapper>
); );
} }

View File

@@ -1,9 +1,36 @@
import { LoaderCustom, TextCustom } from "@/components";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper"; import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import FloatingButton from "@/components/Button/FloatingButton"; import FloatingButton from "@/components/Button/FloatingButton";
import Event_BoxPublishSection from "@/screens/Event/BoxPublishSection"; import Event_BoxPublishSection from "@/screens/Event/BoxPublishSection";
import { router } from "expo-router"; import { apiEventGetAll } from "@/service/api-client/api-event";
import { dateTimeView } from "@/utils/dateTimeView";
import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function EventBeranda() { export default function EventBeranda() {
const [listData, setListData] = useState([]);
const [isLoadData, setIsLoadData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [])
);
const onLoadData = async () => {
try {
setIsLoadData(true);
const response = await apiEventGetAll({category: "beranda"});
// console.log("Response", JSON.stringify(response.data, null, 2));
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadData(false);
}
};
return ( return (
<ViewWrapper <ViewWrapper
hideFooter hideFooter
@@ -11,13 +38,24 @@ export default function EventBeranda() {
<FloatingButton onPress={() => router.push("/event/create")} /> <FloatingButton onPress={() => router.push("/event/create")} />
} }
> >
{Array.from({ length: 10 }).map((_, index) => ( {isLoadData ? (
<Event_BoxPublishSection <LoaderCustom />
key={index} ) : _.isEmpty(listData) ? (
id={index.toString()} <TextCustom align="center">Belum ada event</TextCustom>
href={`/event/${index}/publish`} ) : (
/> listData.map((item: any, index) => (
))} <Event_BoxPublishSection
key={index}
href={`/event/${item.id}/publish`}
data={item}
rightComponentAvatar={
<TextCustom>
{dateTimeView({ date: item?.tanggal, withoutTime: true })}
</TextCustom>
}
/>
))
)}
</ViewWrapper> </ViewWrapper>
); );
} }

View File

@@ -1,27 +1,57 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
BoxWithHeaderSection, BoxWithHeaderSection,
Grid, Grid,
ScrollableCustom, LoaderCustom,
StackCustom, ScrollableCustom,
TextCustom StackCustom,
TextCustom,
} from "@/components"; } from "@/components";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper"; import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import { useAuth } from "@/hooks/use-auth";
import { dummyMasterStatus } from "@/lib/dummy-data/_master/status"; import { dummyMasterStatus } from "@/lib/dummy-data/_master/status";
import { useState } from "react"; import { apiEventGetByStatus } from "@/service/api-client/api-event";
import { useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function EventStatus() { export default function EventStatus() {
const id = "test-id-event"; 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 apiEventGetByStatus({
id: id!,
status: activeCategory!,
});
// console.log("Response", JSON.stringify(response.data, null, 2));
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.
}; };
const scrollComponent = ( const tabsComponent = (
<ScrollableCustom <ScrollableCustom
data={dummyMasterStatus.map((e, i) => ({ data={dummyMasterStatus.map((e, i) => ({
id: i, id: i,
@@ -34,30 +64,36 @@ export default function EventStatus() {
); );
return ( return (
<ViewWrapper headerComponent={scrollComponent}> <ViewWrapper headerComponent={tabsComponent}>
<BoxWithHeaderSection href={`/event/${id}/${activeCategory}/detail-event`}> {loadingGetData ? (
<StackCustom gap={"xs"}> <LoaderCustom />
<Grid> ) : _.isEmpty(listData) ? (
<Grid.Col span={8}> <TextCustom align="center">Tidak ada data {activeCategory}</TextCustom>
<TextCustom truncate bold> ) : (
Lorem ipsum,{" "} listData.map((item: any, i) => (
<TextCustom color="green">{activeCategory}</TextCustom> dolor <BoxWithHeaderSection
sit amet consectetur adipisicing elit. key={i}
</TextCustom> href={`/event/${item.id }/${activeCategory}/detail-event`}
</Grid.Col> >
<Grid.Col span={4} style={{ alignItems: "flex-end" }}> <StackCustom gap={"xs"}>
<TextCustom>{new Date().toLocaleDateString()}</TextCustom> <Grid>
</Grid.Col> <Grid.Col span={8}>
</Grid> <TextCustom truncate bold>
{item?.title}
</TextCustom>
</Grid.Col>
<Grid.Col span={4} style={{ alignItems: "flex-end" }}>
<TextCustom>
{new Date(item?.tanggal).toLocaleDateString()}
</TextCustom>
</Grid.Col>
</Grid>
<TextCustom truncate={2}> <TextCustom truncate={2}>{item?.deskripsi}</TextCustom>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Consectetur </StackCustom>
eveniet ab eum ducimus tempore a quia deserunt quisquam. Tempora, </BoxWithHeaderSection>
atque. Aperiam minima asperiores dicta perferendis quis adipisci, ))
dolore optio porro! )}
</TextCustom>
</StackCustom>
</BoxWithHeaderSection>
</ViewWrapper> </ViewWrapper>
); );
} }

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
BaseBox, BaseBox,
DotButton, DotButton,
@@ -11,17 +12,64 @@ import {
} from "@/components"; } from "@/components";
import { IMenuDrawerItem } from "@/components/_Interface/types"; import { IMenuDrawerItem } from "@/components/_Interface/types";
import LeftButtonCustom from "@/components/Button/BackButton"; import LeftButtonCustom from "@/components/Button/BackButton";
import Event_AlertButtonStatusSection from "@/screens/Event/AlertButtonStatusSection";
import Event_ButtonStatusSection from "@/screens/Event/ButtonStatusSection"; import Event_ButtonStatusSection from "@/screens/Event/ButtonStatusSection";
import { menuDrawerDraftEvent } from "@/screens/Event/menuDrawerDraft"; import { menuDrawerDraftEvent } from "@/screens/Event/menuDrawerDraft";
import { router, Stack, useLocalSearchParams } from "expo-router"; import { apiEventGetOne } from "@/service/api-client/api-event";
import { useState } from "react"; import { dateTimeView } from "@/utils/dateTimeView";
import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import { useCallback, useState } from "react";
export default function EventDetailStatus() { export default function EventDetailStatus() {
const { id, status } = useLocalSearchParams(); const { id, status } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = useState(false); const [openDrawer, setOpenDrawer] = useState(false);
const [openAlert, setOpenAlert] = useState(false); // const [openAlert, setOpenAlert] = useState(false);
const [openDeleteAlert, setOpenDeleteAlert] = useState(false);
const [data, setData] = useState<any>();
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
async function onLoadData() {
try {
const response = await apiEventGetOne({ id: id as string });
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
}
}
const listData = [
{
title: "Lokasi",
value: data?.lokasi || "-",
},
{
title: "Tipe Acara",
value: data?.EventMaster_TipeAcara?.name || "-",
},
{
title: "Tanggal Mulai",
value: dateTimeView({ date: data?.tanggal }) || "-",
},
{
title: "Tanggal Berakhir",
value: dateTimeView({ date: data?.tanggalSelesai }) || "-",
},
{
title: "Deskripsi",
value: data?.deskripsi || "-",
},
];
const handlePress = (item: IMenuDrawerItem) => { const handlePress = (item: IMenuDrawerItem) => {
console.log("PATH >> ", item.path); console.log("PATH >> ", item.path);
@@ -45,7 +93,7 @@ export default function EventDetailStatus() {
<BaseBox> <BaseBox>
<StackCustom> <StackCustom>
<TextCustom bold align="center" size="xlarge"> <TextCustom bold align="center" size="xlarge">
Judul event {status} {data?.title || "-"}
</TextCustom> </TextCustom>
{listData.map((item, index) => ( {listData.map((item, index) => (
<Grid key={index}> <Grid key={index}>
@@ -60,9 +108,8 @@ export default function EventDetailStatus() {
</StackCustom> </StackCustom>
</BaseBox> </BaseBox>
<Event_ButtonStatusSection <Event_ButtonStatusSection
id={id as string}
status={status as string} status={status as string}
onOpenAlert={setOpenAlert}
onOpenDeleteAlert={setOpenDeleteAlert}
/> />
<Spacing /> <Spacing />
</ViewWrapper> </ViewWrapper>
@@ -70,7 +117,7 @@ export default function EventDetailStatus() {
<DrawerCustom <DrawerCustom
isVisible={openDrawer} isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)} closeDrawer={() => setOpenDrawer(false)}
height={250} height={"auto"}
> >
<MenuDrawerDynamicGrid <MenuDrawerDynamicGrid
data={menuDrawerDraftEvent({ id: id as string }) as any} data={menuDrawerDraftEvent({ id: id as string }) as any}
@@ -78,40 +125,6 @@ export default function EventDetailStatus() {
onPressItem={handlePress as any} onPressItem={handlePress as any}
/> />
</DrawerCustom> </DrawerCustom>
<Event_AlertButtonStatusSection
id={id as string}
status={status as string}
openAlert={openAlert}
setOpenAlert={setOpenAlert}
openDeleteAlert={openDeleteAlert}
setOpenDeleteAlert={setOpenDeleteAlert}
/>
</> </>
); );
} }
const listData = [
{
title: "Lokasi",
value:
"Lorem ipsum dolor sit amet consectetur adipisicing elit. Consectetur eveniet ab eum ducimus tempore a quia deserunt quisquam. Tempora, atque. Aperiam minima asperiores dicta perferendis quis adipisci, dolore optio porro!",
},
{
title: "Tipe Acara",
value: "Workshop",
},
{
title: "Tanggal Mulai",
value: "Senin, 18 Juli 2025, 10:00 WIB",
},
{
title: "Tanggal Berakhir",
value: "Selasa, 19 Juli 2025, 12:00 WIB",
},
{
title: "Deskripsi",
value:
"Lorem ipsum dolor sit amet consectetur adipisicing elit. Consectetur eveniet ab eum ducimus tempore a quia deserunt quisquam. Tempora, atque. Aperiam minima asperiores dicta perferendis quis adipisci, dolore optio porro!",
},
];

View File

@@ -1,20 +1,43 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
DotButton, DotButton,
DrawerCustom, DrawerCustom,
LoaderCustom,
MenuDrawerDynamicGrid, MenuDrawerDynamicGrid,
ViewWrapper,
Spacing, Spacing,
ViewWrapper,
} from "@/components"; } from "@/components";
import { IMenuDrawerItem } from "@/components/_Interface/types"; import { IMenuDrawerItem } from "@/components/_Interface/types";
import LeftButtonCustom from "@/components/Button/BackButton"; import LeftButtonCustom from "@/components/Button/BackButton";
import Event_BoxDetailPublishSection from "@/screens/Event/BoxDetailPublishSection"; import Event_BoxDetailPublishSection from "@/screens/Event/BoxDetailPublishSection";
import { menuDrawerPublishEvent } from "@/screens/Event/menuDrawerPublish"; import { menuDrawerPublishEvent } from "@/screens/Event/menuDrawerPublish";
import { apiEventGetOne } from "@/service/api-client/api-event";
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 EventDetailContribution() { export default function EventDetailContribution() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = useState(false); const [openDrawer, setOpenDrawer] = useState(false);
const [data, setData] = useState<any>();
const [isLoadData, setIsLoadData] = useState(false);
useEffect(() => {
onLoadData();
}, [id]);
const onLoadData = async () => {
try {
setIsLoadData(true);
const response = await apiEventGetOne({ id: id as string });
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadData(false);
}
};
const handlePress = (item: IMenuDrawerItem) => { const handlePress = (item: IMenuDrawerItem) => {
console.log("PATH ", item.path); console.log("PATH ", item.path);
@@ -32,18 +55,22 @@ export default function EventDetailContribution() {
}} }}
/> />
<ViewWrapper> <ViewWrapper>
<Event_BoxDetailPublishSection /> {isLoadData ? (
<LoaderCustom />
) : (
<Event_BoxDetailPublishSection data={data} />
)}
<Spacing /> <Spacing />
</ViewWrapper> </ViewWrapper>
<DrawerCustom <DrawerCustom
isVisible={openDrawer} isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)} closeDrawer={() => setOpenDrawer(false)}
height={250} height={"auto"}
> >
<MenuDrawerDynamicGrid <MenuDrawerDynamicGrid
data={menuDrawerPublishEvent({ id: id as string })} data={menuDrawerPublishEvent({ id: id as string })}
columns={4} columns={4}
onPressItem={handlePress} onPressItem={handlePress as any}
/> />
</DrawerCustom> </DrawerCustom>
</> </>

View File

@@ -1,106 +1,271 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
ButtonCustom, ButtonCustom,
LoaderCustom,
SelectCustom, SelectCustom,
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 { masterTypeEvent } from "@/lib/dummy-data/event/master-type-event"; import {
apiEventGetOne,
apiEventUpdateData,
} from "@/service/api-client/api-event";
import { apiMasterEventType } from "@/service/api-client/api-master";
import { DateTimePickerEvent } from "@react-native-community/datetimepicker"; import { DateTimePickerEvent } from "@react-native-community/datetimepicker";
import { router } from "expo-router"; import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import React, { useState } from "react"; import React, { useCallback, useEffect, useState } from "react";
import { Platform } from "react-native"; import Toast from "react-native-toast-message";
export default function EventEdit() { export default function EventEdit() {
const { id } = useLocalSearchParams();
const [data, setData] = useState<any>();
// {
// title: "",
// lokasi: "",
// deskripsi: "",
// eventMaster_TipeAcaraId: "",
// tanggal: "",
// tanggalSelesai: "",
// authorId: "",
// }
const [listTypeEvent, setListTypeEvent] = useState([]);
const [selectedDate, setSelectedDate] = useState< const [selectedDate, setSelectedDate] = useState<
Date | DateTimePickerEvent | null Date | DateTimePickerEvent | null
>(null); >();
const [selectedEndDate, setSelectedEndDate] = useState< const [selectedEndDate, setSelectedEndDate] = useState<
Date | DateTimePickerEvent | null Date | DateTimePickerEvent | null
>(null); >();
const handlerSubmit = () => { const [isLoading, setIsLoading] = useState(false);
const [isLoadData, setIsLoadData] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
async function onLoadData() {
try { try {
if (selectedDate) { setIsLoadData(true);
console.log("Tanggal yang dipilih:", selectedDate); const response = await apiEventGetOne({ id: id as string });
console.log(`ISO Format ${Platform.OS}:`, selectedDate.toString()); if (response.success) {
// Kirim ke API atau proses lanjutan setData(response.data);
} else { setSelectedDate(new Date(response.data.tanggal));
console.log("Tanggal belum dipilih"); setSelectedEndDate(new Date(response.data.tanggalSelesai));
} }
if (selectedEndDate) {
console.log("Tanggal yang dipilih:", selectedEndDate);
console.log(`ISO Format ${Platform.OS}:`, selectedEndDate.toString());
// Kirim ke API atau proses lanjutan
} else {
console.log("Tanggal berakhir belum dipilih");
}
console.log("Data berhasil terupdate");
router.back()
} catch (error) { } catch (error) {
console.log(error); console.log("[ERROR]", error);
} finally {
setIsLoadData(false);
}
}
useEffect(() => {
onLoadMasterEventType();
}, []);
const onLoadMasterEventType = async () => {
try {
const response = await apiMasterEventType();
setListTypeEvent(response.data);
} catch (error) {
console.log("Error onLoadMasterEventType", error);
} }
}; };
const buttonSubmit = ( const validateDate = async () => {
<ButtonCustom title="Update" onPress={handlerSubmit} /> if (
); data?.title === "" ||
data?.lokasi === "" ||
data?.deskripsi === "" ||
data?.eventMaster_TipeAcaraId === ""
) {
Toast.show({
type: "info",
text1: "Info",
text2: "Lengkapi semua data",
});
return false;
}
const startDate = new Date(selectedDate as any);
const endDate = new Date(selectedEndDate as any);
if (startDate >= endDate) {
Toast.show({
type: "info",
text1: "Info",
text2: "Ubah tanggal berakhirnya event",
});
return false;
}
return true;
};
const handlerSubmit = async () => {
const isValid = await validateDate();
if (!isValid) return;
try {
setIsLoading(true);
const newData = {
...data,
tanggal: new Date(selectedDate as any).toISOString(),
tanggalSelesai: new Date(selectedEndDate as any).toISOString(),
};
const response = await apiEventUpdateData({
id: id as string,
data: newData,
});
if (response.success) {
Toast.show({
type: "success",
text1: response.message,
});
return router.back();
}
} catch (error) {
console.log(error);
} finally {
setIsLoading(false);
}
};
const validateDateRange = (
selectedDate: string | Date,
selectedEndDate: string | Date
): { isValid: boolean; error?: string } => {
const startDate = new Date(selectedDate);
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,
};
};
return ( return (
<> <>
<ViewWrapper> <ViewWrapper>
<StackCustom gap={"xs"}> {isLoadData ? (
<TextInputCustom <LoaderCustom />
placeholder="Masukkan nama event" ) : (
label="Nama Event" <StackCustom gap={"xs"}>
required <TextInputCustom
/> placeholder="Masukkan nama event"
<SelectCustom label="Nama Event"
label="Tipe Event" required
placeholder="Pilih tipe event" value={data?.title}
data={masterTypeEvent} onChangeText={(value) => setData({ ...data, title: value })}
onChange={(value) => console.log(value)} />
/> <SelectCustom
<TextInputCustom label="Tipe Event"
label="Lokasi" placeholder="Pilih tipe event"
placeholder="Masukkan lokasi event" data={listTypeEvent.map((item: any) => ({
required label: item.name,
/> value: item.id,
}))}
value={data?.eventMaster_TipeAcaraId || ""}
onChange={(value) => {
console.log(value);
setData({ ...data, eventMaster_TipeAcaraId: value });
}}
/>
<TextInputCustom
label="Lokasi"
placeholder="Masukkan lokasi event"
required
value={data?.lokasi}
onChangeText={(value) => setData({ ...data, lokasi: value })}
/>
<DateTimePickerCustom
minimumDate={new Date(Date.now())}
label="Tanggal & Waktu Mulai"
required
value={selectedDate as any}
onChange={(date: any) => {
setSelectedDate(date as any);
}}
/>
<StackCustom gap={0}>
<DateTimePickerCustom
minimumDate={selectedDate as any}
label="Tanggal & Waktu Berakhir"
required
value={selectedEndDate as any}
onChange={(date: any) => {
setSelectedEndDate(date as any);
}}
/>
<DateTimePickerCustom {/* Muncul */}
label="Tanggal & Waktu Mulai" {validateDateRange(selectedDate as any, selectedEndDate as any)
required .isValid ? (
onChange={(date: Date) => { <TextCustom style={{ color: "green" }}>
setSelectedDate(date as any); {
}} validateDateRange(
value={selectedDate as any} selectedDate as any,
minimumDate={new Date(Date.now())} selectedEndDate as any
/> ).error
}
</TextCustom>
) : (
<TextCustom style={{ color: "red" }}>
{
validateDateRange(
selectedDate as any,
selectedEndDate as any
).error
}
</TextCustom>
)}
<Spacing />
</StackCustom>
<DateTimePickerCustom <TextAreaCustom
label="Tanggal & Waktu Berakhir" label="Deskripsi"
required placeholder="Masukkan deskripsi event"
onChange={(date: Date) => { required
setSelectedEndDate(date as any); showCount
}} maxLength={100}
value={selectedEndDate as any} value={data?.deskripsi}
/> onChangeText={(value) => setData({ ...data, deskripsi: value })}
/>
<TextAreaCustom <ButtonCustom
label="Deskripsi" isLoading={isLoading}
placeholder="Masukkan deskripsi event" title="Update"
required onPress={handlerSubmit}
showCount />
maxLength={100} </StackCustom>
/> )}
{buttonSubmit}
</StackCustom>
</ViewWrapper> </ViewWrapper>
</> </>
); );

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
DotButton, DotButton,
DrawerCustom, DrawerCustom,
@@ -9,12 +10,29 @@ import { IMenuDrawerItem } from "@/components/_Interface/types";
import LeftButtonCustom from "@/components/Button/BackButton"; import LeftButtonCustom from "@/components/Button/BackButton";
import Event_BoxDetailPublishSection from "@/screens/Event/BoxDetailPublishSection"; import Event_BoxDetailPublishSection from "@/screens/Event/BoxDetailPublishSection";
import { menuDrawerPublishEvent } from "@/screens/Event/menuDrawerPublish"; import { menuDrawerPublishEvent } from "@/screens/Event/menuDrawerPublish";
import { apiEventGetOne } from "@/service/api-client/api-event";
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 EventDetailHistory() { export default function EventDetailHistory() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = useState(false); const [openDrawer, setOpenDrawer] = useState(false);
const [data, setData] = useState();
useEffect(() => {
onLoadData();
}, [id]);
const onLoadData = async () => {
try {
const response = await apiEventGetOne({ id: id as string });
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
const handlePress = (item: IMenuDrawerItem) => { const handlePress = (item: IMenuDrawerItem) => {
console.log("PATH ", item.path); console.log("PATH ", item.path);
@@ -32,7 +50,7 @@ export default function EventDetailHistory() {
}} }}
/> />
<ViewWrapper> <ViewWrapper>
<Event_BoxDetailPublishSection /> <Event_BoxDetailPublishSection data={data} />
<Spacing /> <Spacing />
</ViewWrapper> </ViewWrapper>
<DrawerCustom <DrawerCustom
@@ -43,7 +61,7 @@ export default function EventDetailHistory() {
<MenuDrawerDynamicGrid <MenuDrawerDynamicGrid
data={menuDrawerPublishEvent({ id: id as string })} data={menuDrawerPublishEvent({ id: id as string })}
columns={4} columns={4}
onPressItem={handlePress} onPressItem={handlePress as any}
/> />
</DrawerCustom> </DrawerCustom>
</> </>

View File

@@ -1,17 +1,102 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
AvatarUsernameAndOtherComponent, AvatarUsernameAndOtherComponent,
BadgeCustom,
BaseBox, BaseBox,
LoaderCustom,
TextCustom,
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import {
apiEventGetOne,
apiEventListOfParticipants,
} from "@/service/api-client/api-event";
import { useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import { View } from "react-native";
export default function EventListOfParticipants() { export default function EventListOfParticipants() {
const { id } = useLocalSearchParams();
const [startDate, setStartDate] = useState();
const [listData, setListData] = useState([]);
const [isLoadData, setIsLoadData] = useState(false);
useEffect(() => {
handlerLoadData();
}, [id]);
const handlerLoadData = () => {
try {
setIsLoadData(true);
onLoadData();
onLoadList();
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadData(false);
}
};
const onLoadData = async () => {
try {
const response = await apiEventGetOne({ id: id as string });
if (response.success) {
setStartDate(response.data.tanggal);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
const onLoadList = async () => {
try {
const response = await apiEventListOfParticipants({ id: id as string });
if (response.success) {
setListData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
return ( return (
<ViewWrapper> <ViewWrapper>
{Array.from({ length: 10 }).map((_, index) => ( {isLoadData ? (
<BaseBox key={index} paddingBlock={0}> <LoaderCustom />
<AvatarUsernameAndOtherComponent avatarHref={`/profile/${index}`} /> ) : listData.length === 0 ? (
</BaseBox> <TextCustom align="center">Belum ada peserta</TextCustom>
))} ) : (
listData.map((item: any, index: number) => (
<BaseBox key={index}>
<AvatarUsernameAndOtherComponent
avatar={item?.User?.Profile?.imageId}
name={item?.User?.username}
avatarHref={`/profile/${item?.User?.Profile?.id}`}
rightComponent={
new Date().getTime() > new Date(startDate as any).getTime() ? (
<View
style={{
justifyContent: "flex-end",
}}
>
<BadgeCustom color={item?.isPresent ? "green" : "red"}>
{item?.isPresent ? "Hadir" : "Tidak Hadir"}
</BadgeCustom>
</View>
) : (
<View
style={{
justifyContent: "flex-end",
}}
>
<BadgeCustom color="gray">-</BadgeCustom>
</View>
)
}
/>
</BaseBox>
))
)}
</ViewWrapper> </ViewWrapper>
); );
} }

View File

@@ -1,22 +1,73 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
ButtonCustom, AlertDefaultSystem,
DotButton, ButtonCustom,
DrawerCustom, DotButton,
MenuDrawerDynamicGrid, DrawerCustom,
Spacing, LoaderCustom,
ViewWrapper MenuDrawerDynamicGrid,
ViewWrapper,
} from "@/components"; } from "@/components";
import { IMenuDrawerItem } from "@/components/_Interface/types"; import { IMenuDrawerItem } from "@/components/_Interface/types";
import LeftButtonCustom from "@/components/Button/BackButton"; import LeftButtonCustom from "@/components/Button/BackButton";
import { useAuth } from "@/hooks/use-auth";
import Event_BoxDetailPublishSection from "@/screens/Event/BoxDetailPublishSection"; import Event_BoxDetailPublishSection from "@/screens/Event/BoxDetailPublishSection";
import { menuDrawerPublishEvent } from "@/screens/Event/menuDrawerPublish"; import { menuDrawerPublishEvent } from "@/screens/Event/menuDrawerPublish";
import { router, Stack, useLocalSearchParams } from "expo-router"; import {
import { useState } from "react"; apiEventCheckParticipants,
import { Alert } from "react-native"; apiEventGetOne,
apiEventJoin,
} from "@/service/api-client/api-event";
import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
export default function EventDetailPublish() { export default function EventDetailPublish() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const { user } = useAuth();
const [openDrawer, setOpenDrawer] = useState(false); const [openDrawer, setOpenDrawer] = useState(false);
const [isLoadingData, setIsLoadingData] = useState(false);
const [isLoadingJoin, setIsLoadingJoin] = useState(false);
const [data, setData] = useState();
const [isParticipant, setIsParticipant] = useState<boolean | null>(null);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [])
);
async function onLoadData() {
try {
setIsLoadingData(true);
const response = await apiEventGetOne({ id: id as string });
if (response.success) {
setData(response.data);
const responseCheckParticipants = await apiEventCheckParticipants({
id: id as string,
userId: user?.id as string,
});
if (
responseCheckParticipants.success &&
responseCheckParticipants.data
) {
setIsParticipant(true);
}
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadingData(false);
}
}
const handlePress = (item: IMenuDrawerItem) => { const handlePress = (item: IMenuDrawerItem) => {
console.log("PATH ", item.path); console.log("PATH ", item.path);
@@ -24,15 +75,61 @@ export default function EventDetailPublish() {
setOpenDrawer(false); setOpenDrawer(false);
}; };
const footerButton = ( const handlerJoin = async () => {
<ButtonCustom const userId = user?.id;
backgroundColor="green" if (!userId) {
textColor="white" return Toast.show({
onPress={() => Alert.alert("Anda berhasil join event ini")} type: "error",
> text2: "Anda belum login",
Join });
</ButtonCustom> }
);
try {
setIsLoadingJoin(true);
const response = await apiEventJoin({
id: id as string,
userId: userId as string,
});
if (response.success) {
router.navigate(
`/(application)/(user)/event/${id}/list-of-participants`
);
Toast.show({
type: "success",
text1: "Anda berhasil join",
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadingJoin(false);
}
};
const footerButton = () => {
return (
<>
<ButtonCustom
disabled={isParticipant as any}
isLoading={isLoadingJoin}
backgroundColor="green"
textColor="white"
onPress={() =>
AlertDefaultSystem({
title: "Join event",
message: "Anda yakin ingin join sebagai peserta event ?",
textLeft: "Tidak",
textRight: "Ya",
onPressLeft: () => {},
onPressRight: () => handlerJoin(),
})
}
>
{isParticipant ? "Anda sudah tergabung" : "Join"}
</ButtonCustom>
</>
);
};
return ( return (
<> <>
@@ -44,19 +141,25 @@ export default function EventDetailPublish() {
}} }}
/> />
<ViewWrapper> <ViewWrapper>
<Event_BoxDetailPublishSection footerButton={footerButton} /> {isLoadingData ? (
<Spacing /> <LoaderCustom />
) : (
<Event_BoxDetailPublishSection
data={data}
footerButton={footerButton()}
/>
)}
</ViewWrapper> </ViewWrapper>
<DrawerCustom <DrawerCustom
isVisible={openDrawer} isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)} closeDrawer={() => setOpenDrawer(false)}
height={250} height={"auto"}
> >
<MenuDrawerDynamicGrid <MenuDrawerDynamicGrid
data={menuDrawerPublishEvent({ id: id as string })} data={menuDrawerPublishEvent({ id: id as string })}
columns={4} columns={4}
onPressItem={handlePress} onPressItem={handlePress as any}
/> />
</DrawerCustom> </DrawerCustom>
</> </>

View File

@@ -1,19 +1,51 @@
import { import {
ButtonCustom, ButtonCustom,
SelectCustom, SelectCustom,
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 { masterTypeEvent } from "@/lib/dummy-data/event/master-type-event"; import { useAuth } from "@/hooks/use-auth";
import { apiEventCreate } from "@/service/api-client/api-event";
import { apiMasterEventType } from "@/service/api-client/api-master";
import { DateTimePickerEvent } from "@react-native-community/datetimepicker"; import { DateTimePickerEvent } from "@react-native-community/datetimepicker";
import { router } from "expo-router"; import { router } from "expo-router";
import React, { useState } from "react"; import React, { useEffect, useState } from "react";
import { Platform } from "react-native"; import Toast from "react-native-toast-message";
interface EventCreateProps {
title?: string;
lokasi?: string;
deskripsi?: string;
eventMaster_TipeAcaraId?: string;
tanggal?: string;
tanggalSelesai?: string;
authorId?: string;
}
export default function EventCreate() { export default function EventCreate() {
const [data, setData] = useState<EventCreateProps>();
const [listTypeEvent, setListTypeEvent] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const { user } = useAuth();
useEffect(() => {
onLoadMasterEventType();
}, []);
const onLoadMasterEventType = async () => {
try {
const response = await apiMasterEventType();
setListTypeEvent(response.data);
} catch (error) {
console.log("Error onLoadMasterEventType", error);
}
};
const [selectedDate, setSelectedDate] = useState< const [selectedDate, setSelectedDate] = useState<
Date | DateTimePickerEvent | null Date | DateTimePickerEvent | null
>(null); >(null);
@@ -22,35 +54,76 @@ export default function EventCreate() {
Date | DateTimePickerEvent | null Date | DateTimePickerEvent | null
>(null); >(null);
const handlerSubmit = () => { const handlerSubmit = async () => {
if (
!data?.title ||
!data?.lokasi ||
!data?.deskripsi ||
!data?.eventMaster_TipeAcaraId
) {
Toast.show({
type: "info",
text1: "Info",
text2: "Lengkapi semua data",
});
return;
}
if (!selectedDate || !selectedEndDate) {
Toast.show({
type: "info",
text1: "Info",
text2: "Pilih tanggal mulai dan berakhir",
});
return;
}
// if (selectedDate) {
// console.log("Tanggal yang dipilih:", selectedDate);
// console.log(`ISO Format ${Platform.OS}:`, selectedDate.toString());
// // Kirim ke API atau proses lanjutan
// } else {
// console.log("Tanggal belum dipilih");
// }
// if (selectedEndDate) {
// console.log("Tanggal yang dipilih:", selectedEndDate);
// console.log(`ISO Format ${Platform.OS}:`, selectedEndDate.toString());
// // Kirim ke API atau proses lanjutan
// } else {
// console.log("Tanggal berakhir belum dipilih");
// }
try { try {
if (selectedDate) { setIsLoading(true);
console.log("Tanggal yang dipilih:", selectedDate);
console.log(`ISO Format ${Platform.OS}:`, selectedDate.toString());
// Kirim ke API atau proses lanjutan
} else {
console.log("Tanggal belum dipilih");
}
if (selectedEndDate) { const newData = {
console.log("Tanggal yang dipilih:", selectedEndDate); ...data,
console.log(`ISO Format ${Platform.OS}:`, selectedEndDate.toString()); tanggal: new Date(selectedDate as any).toISOString(),
// Kirim ke API atau proses lanjutan tanggalSelesai: new Date(selectedEndDate as any).toISOString(),
} else { authorId: user?.id,
console.log("Tanggal berakhir belum dipilih"); };
}
console.log("Data berhasil disimpan", JSON.stringify(newData, null, 2));
const response = await apiEventCreate(newData);
console.log("Response", JSON.stringify(response, null, 2));
console.log("Data berhasil disimpan");
router.navigate("/event/status"); router.navigate("/event/status");
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} finally {
setIsLoading(false);
} }
}; };
const buttonSubmit = ( const buttonSubmit = (
<ButtonCustom title="Simpan" onPress={handlerSubmit} /> <ButtonCustom
// <BoxButtonOnFooter> isLoading={isLoading}
// </BoxButtonOnFooter> title="Simpan"
onPress={handlerSubmit}
/>
); );
return ( return (
@@ -61,17 +134,27 @@ export default function EventCreate() {
placeholder="Masukkan nama event" placeholder="Masukkan nama event"
label="Nama Event" label="Nama Event"
required required
onChangeText={(value: any) => setData({ ...data, title: value })}
/> />
<SelectCustom <SelectCustom
label="Tipe Event" label="Tipe Event"
placeholder="Pilih tipe event" placeholder="Pilih tipe event"
data={masterTypeEvent} data={listTypeEvent.map((item: any) => ({
onChange={(value) => console.log(value)} label: item.name,
value: item.id,
}))}
value={data?.eventMaster_TipeAcaraId || ""}
onChange={(value: any) =>
setData({ ...data, eventMaster_TipeAcaraId: value })
}
/> />
<TextInputCustom <TextInputCustom
label="Lokasi" label="Lokasi"
placeholder="Masukkan lokasi event" placeholder="Masukkan lokasi event"
required required
onChangeText={(value: any) => setData({ ...data, lokasi: value })}
/> />
<DateTimePickerCustom <DateTimePickerCustom
@@ -84,14 +167,24 @@ export default function EventCreate() {
minimumDate={new Date(Date.now())} minimumDate={new Date(Date.now())}
/> />
<DateTimePickerCustom <StackCustom gap={0}>
label="Tanggal & Waktu Berakhir" <DateTimePickerCustom
required disabled={!selectedDate}
onChange={(date: Date) => { label="Tanggal & Waktu Berakhir"
setSelectedEndDate(date as any); required
}} onChange={(date: Date) => {
value={selectedEndDate as any} setSelectedEndDate(date as any);
/> }}
value={selectedEndDate as any}
minimumDate={new Date(selectedDate as any)}
/>
{!selectedDate && (
<TextCustom color="gray" size={"small"}>
Note: Pilih tanggal mulai terlebih dahulu
</TextCustom>
)}
<Spacing />
</StackCustom>
<TextAreaCustom <TextAreaCustom
label="Deskripsi" label="Deskripsi"
@@ -99,6 +192,9 @@ export default function EventCreate() {
required required
showCount showCount
maxLength={100} maxLength={100}
onChangeText={(value: any) =>
setData({ ...data, deskripsi: value })
}
/> />
{buttonSubmit} {buttonSubmit}

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { StackCustom, ViewWrapper } from "@/components"; import { StackCustom, ViewWrapper } from "@/components";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
@@ -8,28 +9,40 @@ import TabSection from "@/screens/Home/tabSection";
import { tabsHome } from "@/screens/Home/tabsList"; import { tabsHome } from "@/screens/Home/tabsList";
import Home_FeatureSection from "@/screens/Home/topFeatureSection"; import Home_FeatureSection from "@/screens/Home/topFeatureSection";
import { apiUser } from "@/service/api-client/api-user"; import { apiUser } from "@/service/api-client/api-user";
import { apiVersion } from "@/service/api-config";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { Redirect, router, Stack } from "expo-router"; import { Redirect, router, Stack } from "expo-router";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
export default function Application() { export default function Application() {
const { user } = useAuth(); const { token, user } = useAuth();
const [data, setData] = useState<any>({});
const [data, setData] = useState<any>();
useEffect(() => { useEffect(() => {
onLoadData(); onLoadData();
checkVersion();
}, []); }, []);
async function onLoadData() { async function onLoadData() {
const response = await apiUser(user?.id as string); const response = await apiUser(user?.id as string);
console.log("Response profile >>", JSON.stringify(response?.data?.Profile, null, 2));
setData(response.data); setData(response.data);
} }
const checkVersion = async () => {
const response = await apiVersion();
console.log("Version >>", JSON.stringify(response.data, null, 2));
};
if (data && data?.active === false) { if (data && data?.active === false) {
console.log("User is not active");
return <Redirect href={`/waiting-room`} />; return <Redirect href={`/waiting-room`} />;
} }
if (data && data?.Profile === null) { if (data && data?.Profile === null) {
console.log("Profile is null");
return <Redirect href={`/profile/create`} />; return <Redirect href={`/profile/create`} />;
} }
@@ -37,7 +50,7 @@ export default function Application() {
<> <>
<Stack.Screen <Stack.Screen
options={{ options={{
title: "HIPMI", title: `HIPMI`,
headerLeft: () => ( headerLeft: () => (
<Ionicons <Ionicons
name="search" name="search"

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

@@ -1,34 +1,83 @@
import { import {
AvatarUsernameAndOtherComponent, AvatarUsernameAndOtherComponent,
BoxWithHeaderSection, BoxWithHeaderSection,
FloatingButton, FloatingButton,
SearchInput, LoaderCustom,
Spacing, SearchInput,
TextCustom, Spacing,
ViewWrapper StackCustom,
TextCustom,
ViewWrapper,
} from "@/components"; } from "@/components";
import { jobDataDummy } from "@/screens/Job/listDataDummy"; import { apiJobGetAll } from "@/service/api-client/api-job";
import { router } from "expo-router"; import { router, useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function JobBeranda() { export default function JobBeranda() {
const [listData, setListData] = useState<any[]>([]);
const [isLoadData, setIsLoadData] = useState(false);
const [search, setSearch] = useState("");
useFocusEffect(
useCallback(() => {
onLoadData(search);
}, [search])
);
const onLoadData = async (search: string) => {
try {
setIsLoadData(true);
const response = await apiJobGetAll({ search, category: "beranda" });
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadData(false);
}
};
const handleSearch = (search: string) => {
setSearch(search);
onLoadData(search);
};
return ( return (
<ViewWrapper <ViewWrapper
hideFooter hideFooter
floatingButton={ floatingButton={
<FloatingButton onPress={() => router.push("/job/create")} /> <FloatingButton onPress={() => router.push("/job/create")} />
} }
headerComponent={<SearchInput placeholder="Cari pekerjaan" />} headerComponent={
<SearchInput placeholder="Cari pekerjaan" onChangeText={handleSearch} />
}
> >
{jobDataDummy.map((item, index) => ( {isLoadData ? (
<BoxWithHeaderSection key={index} onPress={() => router.push(`/job/${item.id}`)}> <LoaderCustom />
<AvatarUsernameAndOtherComponent avatarHref={`/profile/${item.id}`} /> ) : _.isEmpty(listData) ? (
<Spacing /> <TextCustom align="center">Belum ada lowongan</TextCustom>
<TextCustom truncate={2} align="center" bold size="large"> ) : (
{item.posisi} listData.map((item, index) => (
</TextCustom> <BoxWithHeaderSection
<Spacing /> key={index}
</BoxWithHeaderSection> onPress={() => router.push(`/job/${item.id}`)}
))} >
<StackCustom>
<AvatarUsernameAndOtherComponent
avatar={item?.Author?.Profile?.imageId}
avatarHref={`/profile/${item?.Author?.Profile?.id}`}
name={item?.Author?.username}
/>
<TextCustom truncate={2} align="center" bold size="large">
{item?.title || "-"}
</TextCustom>
</StackCustom>
<Spacing />
</BoxWithHeaderSection>
))
)}
<Spacing />
</ViewWrapper> </ViewWrapper>
); );
} }

View File

@@ -1,17 +1,46 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
BaseBox, BaseBox,
ScrollableCustom, LoaderCustom,
TextCustom, ScrollableCustom,
ViewWrapper, TextCustom,
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 { jobDataDummy } from "@/screens/Job/listDataDummy"; import { apiJobGetByStatus } from "@/service/api-client/api-job";
import { useState } from "react"; import { useFocusEffect } from "expo-router";
import _ from "lodash";
import { useCallback, useState } from "react";
export default function JobStatus() { export default function JobStatus() {
const { user } = useAuth();
const [activeCategory, setActiveCategory] = useState<string | null>( const [activeCategory, setActiveCategory] = useState<string | null>(
"publish" "publish"
); );
const [listData, setListData] = useState<any[]>([]);
const [isLoadList, setIsLoadList] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [user?.id, activeCategory])
);
const onLoadData = async () => {
try {
setIsLoadList(true);
const response = await apiJobGetByStatus({
authorId: user?.id as string,
status: activeCategory as string,
});
setListData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadList(false);
}
};
const handlePress = (item: any) => { const handlePress = (item: any) => {
setActiveCategory(item.value); setActiveCategory(item.value);
@@ -32,19 +61,24 @@ export default function JobStatus() {
return ( return (
<ViewWrapper headerComponent={scrollComponent} hideFooter> <ViewWrapper headerComponent={scrollComponent} hideFooter>
{jobDataDummy.map((e, i) => ( {isLoadList ? (
<BaseBox <LoaderCustom />
key={i} ) : _.isEmpty(listData) ? (
paddingTop={20} <TextCustom align="center">Tidak ada data {activeCategory}</TextCustom>
paddingBottom={20} ) : (
href={`/job/${e.id}/${activeCategory}/detail`} listData.map((e, i) => (
// onPress={() => console.log("pressed")} <BaseBox
> key={i}
<TextCustom align="center" bold truncate size="large"> paddingTop={20}
{e.posisi} {activeCategory?.toUpperCase()} paddingBottom={20}
</TextCustom> href={`/job/${e?.id}/${activeCategory}/detail`}
</BaseBox> >
))} <TextCustom align="center" bold truncate size="large">
{e?.title}
</TextCustom>
</BaseBox>
))
)}
</ViewWrapper> </ViewWrapper>
); );
} }

View File

@@ -1,23 +1,51 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
BackButton, BackButton,
DotButton, DotButton,
DrawerCustom, DrawerCustom,
LoaderCustom,
MenuDrawerDynamicGrid, MenuDrawerDynamicGrid,
Spacing, Spacing,
StackCustom,
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import { IconEdit } from "@/components/_Icon"; import { IconEdit } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types"; import { IMenuDrawerItem } from "@/components/_Interface/types";
import Job_BoxDetailSection from "@/screens/Job/BoxDetailSection"; import Job_BoxDetailSection from "@/screens/Job/BoxDetailSection";
import Job_ButtonStatusSection from "@/screens/Job/ButtonStatusSection"; import Job_ButtonStatusSection from "@/screens/Job/ButtonStatusSection";
import { jobDataDummy } from "@/screens/Job/listDataDummy"; import { apiJobGetOne } from "@/service/api-client/api-job";
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 JobDetailStatus() { export default function JobDetailStatus() {
const { id, status } = useLocalSearchParams(); const { id, status } = useLocalSearchParams();
const [openDrawer, setOpenDrawer] = useState(false); const [openDrawer, setOpenDrawer] = useState(false);
const jobDetail = jobDataDummy.find((e) => e.id === Number(id)); 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 handlePress = (item: IMenuDrawerItem) => { const handlePress = (item: IMenuDrawerItem) => {
console.log("PATH >> ", item.path); console.log("PATH >> ", item.path);
@@ -38,9 +66,23 @@ export default function JobDetailStatus() {
}} }}
/> />
<ViewWrapper> <ViewWrapper>
<Job_BoxDetailSection data={jobDetail} /> {isLoadData ? (
<Job_ButtonStatusSection status={status as string} /> <LoaderCustom />
<Spacing /> ) : (
<>
<StackCustom>
<Job_BoxDetailSection data={data} />
<Job_ButtonStatusSection
id={id as string}
status={status as string}
isLoading={isLoading}
onSetLoading={setIsLoading}
isArchive={true}
/>
</StackCustom>
<Spacing />
</>
)}
</ViewWrapper> </ViewWrapper>
<DrawerCustom <DrawerCustom

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

@@ -1,21 +1,133 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
BaseBox,
ButtonCenteredOnly, ButtonCenteredOnly,
ButtonCustom, ButtonCustom,
DummyLandscapeImage,
InformationBox, InformationBox,
LandscapeFrameUploaded, LandscapeFrameUploaded,
LoaderCustom,
Spacing, Spacing,
StackCustom, StackCustom,
TextAreaCustom, TextAreaCustom,
TextInputCustom, TextInputCustom,
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import { router } from "expo-router"; import DIRECTORY_ID from "@/constants/directory-id";
import { apiJobGetOne, apiJobUpdateData } from "@/service/api-client/api-job";
import {
deleteImageService,
uploadImageService,
} from "@/service/upload-service";
import pickImage from "@/utils/pickImage";
import { router, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import Toast from "react-native-toast-message";
export default function JobEdit() { export default function JobEdit() {
const { id } = useLocalSearchParams();
const [data, setData] = useState<any>({
title: "",
content: "",
deskripsi: "",
});
const [isLoadData, setIsLoadData] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const [imageUri, setImageUri] = useState<string | null>(null);
useEffect(() => {
onLoadData();
}, [id]);
const onLoadData = async () => {
try {
setIsLoadData(true);
const response = await apiJobGetOne({ id: id as string });
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoadData(false);
}
};
const handlerOnUpdate = async () => {
if (!data.title || !data.content || !data.deskripsi) {
Toast.show({
type: "info",
text1: "Info",
text2: "Harap isi semua data",
});
return;
}
try {
setIsLoading(true);
let newImageId = "";
if (imageUri) {
const responseUploadImage = await uploadImageService({
imageUri: imageUri,
dirId: DIRECTORY_ID.job_image,
});
if (responseUploadImage.success) {
newImageId = responseUploadImage.data.id;
}
}
if (data?.imageId) {
const responseDeleteImage = await deleteImageService({
id: data.imageId,
});
if (!responseDeleteImage.success) {
console.log("[ERROR DELETE IMAGE]", responseDeleteImage.message);
}
}
const newData = {
title: data.title,
content: data.content,
deskripsi: data.deskripsi,
imageId: newImageId,
};
const response = await apiJobUpdateData({
id: id as string,
data: newData,
category: "edit",
});
if (response.success) {
Toast.show({
type: "success",
text1: response.message,
});
router.back();
} else {
Toast.show({
type: "info",
text1: "Info",
text2: response.message,
});
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
};
const buttonSubmit = () => { const buttonSubmit = () => {
return ( return (
<> <>
<ButtonCustom onPress={() => router.back()}>Update</ButtonCustom> <ButtonCustom isLoading={isLoading} onPress={() => handlerOnUpdate()}>
Update
</ButtonCustom>
<Spacing /> <Spacing />
</> </>
); );
@@ -23,45 +135,64 @@ export default function JobEdit() {
return ( return (
<ViewWrapper> <ViewWrapper>
<StackCustom gap={"xs"}> {isLoadData ? (
<InformationBox text="Poster atau gambar lowongan kerja bersifat opsional, tidak wajib untuk dimasukkan dan upload lah gambar yang sesuai dengan deskripsi lowongan kerja." /> <LoaderCustom />
) : (
<StackCustom gap={"xs"}>
<InformationBox text="Poster atau gambar lowongan kerja bersifat opsional, tidak wajib untuk dimasukkan dan upload lah gambar yang sesuai dengan deskripsi lowongan kerja." />
<LandscapeFrameUploaded /> {imageUri ? (
<ButtonCenteredOnly <LandscapeFrameUploaded image={imageUri as any} />
onPress={() => { ) : (
router.push("/(application)/(image)/take-picture/123"); <BaseBox>
}} <DummyLandscapeImage imageId={data?.imageId} />
icon="upload" </BaseBox>
> )}
Upload
</ButtonCenteredOnly>
<Spacing /> <ButtonCenteredOnly
onPress={() => {
pickImage({
setImageUri,
});
}}
icon="upload"
>
Upload
</ButtonCenteredOnly>
<TextInputCustom <Spacing />
label="Judul Lowongan"
placeholder="Masukan Judul Lowongan Kerja"
required
/>
<TextAreaCustom <TextInputCustom
label="Syarat & Kualifikasi" label="Judul Lowongan"
placeholder="Masukan Syarat & Kualifikasi Lowongan Kerja" placeholder="Masukan Judul Lowongan Kerja"
required required
showCount value={data.title}
maxLength={1000} onChangeText={(value) => setData({ ...data, title: value })}
/> />
<TextAreaCustom <TextAreaCustom
label="Deskripsi Lowongan" label="Syarat & Kualifikasi"
placeholder="Masukan Deskripsi Lowongan Kerja" placeholder="Masukan Syarat & Kualifikasi Lowongan Kerja"
required required
showCount showCount
maxLength={1000} maxLength={1000}
/> value={data.content}
onChangeText={(value) => setData({ ...data, content: value })}
/>
{buttonSubmit()} <TextAreaCustom
</StackCustom> label="Deskripsi Lowongan"
placeholder="Masukan Deskripsi Lowongan Kerja"
required
showCount
maxLength={1000}
value={data.deskripsi}
onChangeText={(value) => setData({ ...data, deskripsi: value })}
/>
{buttonSubmit()}
</StackCustom>
)}
</ViewWrapper> </ViewWrapper>
); );
} }

View File

@@ -1,25 +1,41 @@
import { /* eslint-disable react-hooks/exhaustive-deps */
ButtonCustom, import { ButtonCustom, LoaderCustom, Spacing, StackCustom, ViewWrapper } from "@/components";
Spacing,
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 { jobDataDummy } from "@/screens/Job/listDataDummy"; import { apiJobGetOne } from "@/service/api-client/api-job";
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";
import { useEffect, useState } from "react";
import { Alert, Linking } from "react-native"; import { Alert, Linking } from "react-native";
export default function JobDetail() { export default function JobDetail() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const [data, setData] = useState<any>(null);
const [isLoading, setIsLoading] = useState(false);
const jobDetail = jobDataDummy.find((e) => e.id === Number(id)); useEffect(() => {
onLoadData();
}, [id]);
const OpenLinkButton = () => { const onLoadData = async () => {
const jobUrl = try {
"https://stg-hipmi.wibudev.com/job-vacancy/cm6ijt9w8005zucv4twsct657"; setIsLoading(true);
const response = await apiJobGetOne({ id: id as string });
setData(response.data);
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
};
const linkUrl = `http://192.168.1.83:3000/job-vacancy/`;
const OpenLinkButton = ({ id }: { id: string }) => {
const jobUrl = `${linkUrl}${id}`;
const openInBrowser = async () => { const openInBrowser = async () => {
const supported = await Linking.canOpenURL(jobUrl); const supported = await Linking.canOpenURL(jobUrl);
@@ -44,9 +60,8 @@ export default function JobDetail() {
); );
}; };
const CopyLinkButton = () => { const CopyLinkButton = ({ id }: { id: string }) => {
const jobUrl = const jobUrl = `${linkUrl}${id}`;
"https://stg-hipmi.wibudev.com/job-vacancy/cm6ijt9w8005zucv4twsct657";
const copyToClipboard = async () => { const copyToClipboard = async () => {
await Clipboard.setStringAsync(jobUrl); await Clipboard.setStringAsync(jobUrl);
@@ -70,10 +85,18 @@ export default function JobDetail() {
return ( return (
<ViewWrapper> <ViewWrapper>
<Job_BoxDetailSection data={jobDetail}/> {isLoading ? (
<OpenLinkButton /> <LoaderCustom />
<Spacing /> ) : (
<CopyLinkButton /> <>
<Job_BoxDetailSection data={data} />
<StackCustom>
<OpenLinkButton id={id as string} />
<CopyLinkButton id={id as string} />
</StackCustom>
<Spacing />
</>
)}
</ViewWrapper> </ViewWrapper>
); );
} }

View File

@@ -7,19 +7,99 @@ import {
StackCustom, StackCustom,
TextAreaCustom, TextAreaCustom,
TextInputCustom, TextInputCustom,
ViewWrapper, ViewWrapper
} from "@/components"; } from "@/components";
import DIRECTORY_ID from "@/constants/directory-id";
import { useAuth } from "@/hooks/use-auth";
import { apiJobCreate } from "@/service/api-client/api-job";
import { uploadImageService } from "@/service/upload-service";
import pickImage from "@/utils/pickImage";
import { router } from "expo-router"; import { router } from "expo-router";
import { useState } from "react";
import Toast from "react-native-toast-message";
export default function JobCreate() { export default function JobCreate() {
const nextUrl = "/(application)/(user)/job/(tabs)/status";
const { user } = useAuth();
const [isLoading, setIsLoading] = useState(false);
const [image, setImage] = useState<string | null>(null);
const [data, setData] = useState({
title: "",
content: "",
deskripsi: "",
authorId: "",
});
const handlerOnSubmit = async () => {
let imageId = "";
const newData = {
title: data.title,
content: data.content,
deskripsi: data.deskripsi,
authorId: user?.id,
imageId: "",
};
if (!data.title || !data.content || !data.deskripsi || !user?.id) {
Toast.show({
type: "info",
text1: "Info",
text2: "Harap isi semua data",
});
return;
}
try {
setIsLoading(true);
if (image === null || !image) {
const response = await apiJobCreate(newData);
if (response.success) {
Toast.show({
type: "success",
text1: "Berhasil",
text2: "Lowongan berhasil dibuat",
});
router.replace(nextUrl);
}
return;
}
const responseUploadImage = await uploadImageService({
imageUri: image,
dirId: DIRECTORY_ID.job_image,
});
if (responseUploadImage.success) {
imageId = responseUploadImage.data.id;
}
const fixData = {
...newData,
imageId: imageId,
};
const response = await apiJobCreate(fixData);
if (response.success) {
Toast.show({
type: "success",
text1: "Berhasil",
text2: "Lowongan berhasil dibuat",
});
router.replace(nextUrl);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setIsLoading(false);
}
};
const buttonSubmit = () => { const buttonSubmit = () => {
return ( return (
<> <>
<ButtonCustom <ButtonCustom isLoading={isLoading} onPress={() => handlerOnSubmit()}>
onPress={() =>
router.replace("/(application)/(user)/job/(tabs)/status")
}
>
Simpan Simpan
</ButtonCustom> </ButtonCustom>
<Spacing /> <Spacing />
@@ -32,10 +112,19 @@ export default function JobCreate() {
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
<InformationBox text="Poster atau gambar lowongan kerja bersifat opsional, tidak wajib untuk dimasukkan dan upload lah gambar yang sesuai dengan deskripsi lowongan kerja." /> <InformationBox text="Poster atau gambar lowongan kerja bersifat opsional, tidak wajib untuk dimasukkan dan upload lah gambar yang sesuai dengan deskripsi lowongan kerja." />
<LandscapeFrameUploaded /> {/* <BaseBox>
<Image
source={image ? { uri: image } : DUMMY_IMAGE.dummy_image}
style={{ width: "100%", height: 200 }}
/>
</BaseBox> */}
<LandscapeFrameUploaded image={image as string} />
<ButtonCenteredOnly <ButtonCenteredOnly
onPress={() => { onPress={() => {
router.push("/(application)/(image)/take-picture/123"); // router.push("/(application)/(image)/take-picture/123");
pickImage({
setImageUri: setImage,
});
}} }}
icon="upload" icon="upload"
> >
@@ -48,6 +137,8 @@ export default function JobCreate() {
label="Judul Lowongan" label="Judul Lowongan"
placeholder="Masukan Judul Lowongan Kerja" placeholder="Masukan Judul Lowongan Kerja"
required required
value={data.title}
onChangeText={(value) => setData({ ...data, title: value })}
/> />
<TextAreaCustom <TextAreaCustom
@@ -56,6 +147,8 @@ export default function JobCreate() {
required required
showCount showCount
maxLength={1000} maxLength={1000}
value={data.content}
onChangeText={(value) => setData({ ...data, content: value })}
/> />
<TextAreaCustom <TextAreaCustom
@@ -64,6 +157,8 @@ export default function JobCreate() {
required required
showCount showCount
maxLength={1000} maxLength={1000}
value={data.deskripsi}
onChangeText={(value) => setData({ ...data, deskripsi: value })}
/> />
{buttonSubmit()} {buttonSubmit()}

View File

@@ -1,9 +1,19 @@
import { MapCustom, ViewWrapper } from "@/components"; import { MapCustom, ViewWrapper } from "@/components";
import { View } from "react-native";
import MapView from "react-native-maps";
export default function Maps() { export default function Maps() {
return ( return (
<ViewWrapper style={{ paddingInline: 0, paddingBlock: 0 }}> <ViewWrapper style={{ paddingInline: 0, paddingBlock: 0 }}>
<MapCustom height={"100%"} /> {/* <MapCustom height={"100%"} /> */}
<View style={{ flex: 1 }}>
<MapView
style={{
width: "100%",
height: "100%",
}}
/>
</View>
</ViewWrapper> </ViewWrapper>
); );
} }

View File

@@ -1,10 +1,12 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { import {
BoxButtonOnFooter, ActionIcon,
AvatarComp,
BaseBox,
ButtonCenteredOnly, ButtonCenteredOnly,
ButtonCustom, CenterCustom,
Grid, Grid,
InformationBox, InformationBox,
LandscapeFrameUploaded,
SelectCustom, SelectCustom,
Spacing, Spacing,
StackCustom, StackCustom,
@@ -13,49 +15,122 @@ import {
TextInputCustom, TextInputCustom,
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import { IconPlus } from "@/components/_Icon";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import dummyMasterBidangBisnis from "@/lib/dummy-data/master-bidang-bisnis"; import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
import dummyMasterSubBidangBisnis from "@/lib/dummy-data/master-sub-bidang-bisnis"; import DUMMY_IMAGE from "@/constants/dummy-image-value";
import Portofolio_ButtonCreate from "@/screens/Portofolio/ButtonCreatePortofolio";
import {
apiMasterBidangBisnis,
apiMasterSubBidangBisnis,
} from "@/service/api-client/api-master";
import {
IMasterBidangBisnis,
IMasterSubBidangBisnis,
} from "@/types/Type-Master";
import pickImage from "@/utils/pickImage";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { router, useLocalSearchParams } from "expo-router"; import { Image } from "expo-image";
import { useState } from "react"; import { useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useEffect, useState } from "react";
import { Text, TouchableOpacity, View } from "react-native"; import { Text, TouchableOpacity, View } from "react-native";
import PhoneInput, { ICountry } from "react-native-international-phone-number"; import PhoneInput, { ICountry } from "react-native-international-phone-number";
import { Avatar } from "react-native-paper";
export default function PortofolioCreate() { export default function PortofolioCreate() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const [selectedCountry, setSelectedCountry] = useState<null | ICountry>(null); const [selectedCountry, setSelectedCountry] = useState<null | ICountry>(null);
const [inputValue, setInputValue] = useState<string>(""); const [inputValue, setInputValue] = useState<string>("");
const [data, setData] = useState({ const [data, setData] = useState({
name: "", namaBisnis: "",
bidang_usaha: "", masterBidangBisnisId: "",
sub_bidang_usaha: "", alamatKantor: "",
alamat: "", tlpn: "",
nomor_telepon: "",
deskripsi: "", deskripsi: "",
}); });
const [imageUri, setImageUri] = useState<string | null>(null);
const [bidangBisnis, setBidangBisnis] = useState<IMasterBidangBisnis[]>([]);
const [subBidangBisnis, setSubBidangBisnis] = useState<
IMasterSubBidangBisnis[]
>([]);
const [selectedSubBidang, setSelectedSubBidang] = useState<string[]>([]);
const [listSubBidangSelected, setListSubBidangSelected] = useState([
{
id: "",
},
]);
const [dataMedsos, setDataMedsos] = useState({
facebook: "",
twitter: "",
instagram: "",
youtube: "",
tiktok: "",
});
const [isLoadingCreate, setIsLoadingCreate] = useState(false);
function handleInputValue(phoneNumber: string) { function handleInputValue(phoneNumber: string) {
setInputValue(phoneNumber); setInputValue(phoneNumber);
const callingCode = selectedCountry?.callingCode.replace(/^\+/, "") || "";
const fixNumber = inputValue.replace(/\s+/g, "");
const realNumber = callingCode + fixNumber;
setData({ ...data, tlpn: realNumber });
} }
function handleSelectedCountry(country: ICountry) { function handleSelectedCountry(country: ICountry) {
setSelectedCountry(country); setSelectedCountry(country);
} }
function handleSave() { useEffect(() => {
console.log("Selanjutnya"); onLoadMaster();
router.replace(`/maps/create`); onLoadMasterSubBidangBisnis();
} }, []);
const buttonSave = ( const onLoadMaster = async () => {
<BoxButtonOnFooter> try {
<ButtonCustom onPress={handleSave}>Selanjutnya</ButtonCustom> const response = await apiMasterBidangBisnis();
</BoxButtonOnFooter> setBidangBisnis(response.data);
); } catch (error) {
setBidangBisnis([]);
console.log("Error onLoadMasterBidangBisnis", error);
}
};
const onLoadMasterSubBidangBisnis = async () => {
try {
const response = await apiMasterSubBidangBisnis({});
setSubBidangBisnis(response.data);
} catch (error) {
setSubBidangBisnis([]);
console.log("Error onLoadMasterBidangBisnis", error);
}
};
const handlerSelectedSubBidang = ({ id }: { id: string }) => {
const selectedList = subBidangBisnis?.filter(
(item) => (item?.masterBidangBisnisId as any) === id
);
setSelectedSubBidang(selectedList as any[]);
};
return ( return (
<ViewWrapper footerComponent={buttonSave}> <ViewWrapper
footerComponent={
<Portofolio_ButtonCreate
id={id as string}
data={data}
dataMedsos={dataMedsos}
imageUri={imageUri}
subBidangSelected={listSubBidangSelected}
isLoadingCreate={isLoadingCreate}
setIsLoadingCreate={setIsLoadingCreate}
/>
}
>
{/* <TextCustom>Portofolio Create {id}</TextCustom> */} {/* <TextCustom>Portofolio Create {id}</TextCustom> */}
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
<InformationBox text="Lengkapi data bisnis anda." /> <InformationBox text="Lengkapi data bisnis anda." />
@@ -63,51 +138,121 @@ export default function PortofolioCreate() {
required required
label="Nama Bisnis" label="Nama Bisnis"
placeholder="Masukkan nama bisnis" placeholder="Masukkan nama bisnis"
onChangeText={(value: any) => setData({ ...data, namaBisnis: value })}
/> />
<SelectCustom <SelectCustom
label="Bidang Usaha" label="Bidang Usaha"
required required
data={dummyMasterBidangBisnis.map((item) => ({ data={bidangBisnis.map((item) => ({
label: item.name, label: item.name,
value: item.id, value: item.id,
}))} }))}
value={data.bidang_usaha} value={data.masterBidangBisnisId}
onChange={(value) => { onChange={(value) => {
setData({ ...(data as any), bidang_usaha: value }); const isSameBidang = data.masterBidangBisnisId === value;
if (!isSameBidang) {
setListSubBidangSelected([{ id: "" }]);
}
setData({ ...(data as any), masterBidangBisnisId: value });
handlerSelectedSubBidang({ id: value as string });
}} }}
/> />
<Grid> {listSubBidangSelected.map((item, index) => (
<Grid.Col span={10}> <SelectCustom
<SelectCustom key={index}
// disabled disabled={data.masterBidangBisnisId === ""}
label="Sub Bidang Usaha" label="Sub Bidang Usaha"
required required
data={dummyMasterSubBidangBisnis.map((item) => ({ data={_.map(selectedSubBidang as any)
label: item.name, .filter((option: any) => {
value: item.id, const selectedValues = listSubBidangSelected.map((s) => s.id);
return (
option.id === item.id || // biarkan tetap muncul kalau ini valuenya sendiri
!selectedValues.includes(option.id)
);
})
.map((e: any) => ({
value: e.id,
label: e.name,
}))} }))}
value={data.sub_bidang_usaha} value={item.id || null}
onChange={(value) => { onChange={(value) => {
setData({ ...(data as any), sub_bidang_usaha: value }); const list = _.clone(listSubBidangSelected);
}} list[index].id = value as any;
/> setListSubBidangSelected(list);
</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>
<View style={{ flexDirection: "row", alignItems: "center", gap: 10 }}>
<ActionIcon
disabled={
selectedSubBidang.length === listSubBidangSelected.length
}
onPress={() => {
setListSubBidangSelected([
...listSubBidangSelected,
{ id: "" },
]);
}}
icon={
<Ionicons
name="add-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
<ActionIcon
disabled={listSubBidangSelected.length <= 1}
onPress={() => {
const list = _.clone(listSubBidangSelected);
list.pop();
setListSubBidangSelected(list);
}}
icon={
<Ionicons
name="remove-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
</View>
</CenterCustom>
<Spacing />
{/* <SelectCustom
label="Bidang Usaha"
required
data={bidangBisnis.map((item) => ({
label: item.name,
value: item.id,
}))}
value={null}
onChange={(value) => {
setData({ ...(data as any), masterBidangBisnisId: value });
handlerSelectedSubBidang({ id: value as string });
}}
/> */}
{/* <ButtonCenteredOnly
onPress={() => {
setListSubBidangSelected([...listSubBidangSelected, { id: "" }]);
}}
>
Tambah Pilihan Tambah Pilihan
</ButtonCenteredOnly> </ButtonCenteredOnly>
<Spacing /> <Spacing /> */}
{/* <TextCustom>{JSON.stringify(bidangBisnis, null, 2)}</TextCustom> */}
<View> <View>
<View style={{ flexDirection: "row", alignItems: "center" }}> <View style={{ flexDirection: "row", alignItems: "center" }}>
@@ -132,6 +277,9 @@ export default function PortofolioCreate() {
required required
label="Alamat Bisnis" label="Alamat Bisnis"
placeholder="Masukkan alamat bisnis" placeholder="Masukkan alamat bisnis"
onChangeText={(value: any) =>
setData({ ...data, alamatKantor: value })
}
/> />
<TextAreaCustom <TextAreaCustom
@@ -144,18 +292,26 @@ export default function PortofolioCreate() {
maxRows={5} maxRows={5}
required required
showCount showCount
maxLength={100} maxLength={1000}
/> />
<Spacing /> <Spacing />
{/* Logo */} {/* Logo */}
<InformationBox text="Upload logo bisnis anda untuk di tampilaka pada portofolio." /> <InformationBox text="Upload logo bisnis anda untuk di tampilaka pada portofolio." />
<LandscapeFrameUploaded />
<CenterCustom>
<Avatar.Image
source={imageUri ? { uri: imageUri } : DUMMY_IMAGE.dummy_image}
size={200}
/>
</CenterCustom>
<Spacing />
<ButtonCenteredOnly <ButtonCenteredOnly
icon="upload" icon="upload"
onPress={() => { onPress={() => {
console.log("Upload logo >>", id); pickImage({
router.navigate(`/(application)/(image)/take-picture/${id}`); setImageUri,
});
}} }}
> >
Upload Upload
@@ -167,22 +323,37 @@ export default function PortofolioCreate() {
<TextInputCustom <TextInputCustom
label="Tiktok" label="Tiktok"
placeholder="Masukkan username tiktok" placeholder="Masukkan username tiktok"
onChangeText={(value: any) =>
setDataMedsos({ ...dataMedsos, tiktok: value })
}
/> />
<TextInputCustom <TextInputCustom
label="Facebook" label="Facebook"
placeholder="Masukkan username facebook" placeholder="Masukkan username facebook"
onChangeText={(value: any) =>
setDataMedsos({ ...dataMedsos, facebook: value })
}
/> />
<TextInputCustom <TextInputCustom
label="Instagram" label="Instagram"
placeholder="Masukkan username instagram" placeholder="Masukkan username instagram"
onChangeText={(value: any) =>
setDataMedsos({ ...dataMedsos, instagram: value })
}
/> />
<TextInputCustom <TextInputCustom
label="Twitter" label="Twitter"
placeholder="Masukkan username twitter" placeholder="Masukkan username twitter"
onChangeText={(value: any) =>
setDataMedsos({ ...dataMedsos, twitter: value })
}
/> />
<TextInputCustom <TextInputCustom
label="Youtube" label="Youtube"
placeholder="Masukkan username youtube" placeholder="Masukkan username youtube"
onChangeText={(value: any) =>
setDataMedsos({ ...dataMedsos, youtube: value })
}
/> />
<Spacing /> <Spacing />
</StackCustom> </StackCustom>

View File

@@ -1,25 +1,125 @@
import { import {
AvatarCustom,
BaseBox, BaseBox,
BoxButtonOnFooter, BoxButtonOnFooter,
ButtonCenteredOnly, ButtonCenteredOnly,
ButtonCustom, ButtonCustom,
ViewWrapper, ViewWrapper
} from "@/components"; } from "@/components";
import API_STRORAGE from "@/constants/base-url-api-strorage";
import DIRECTORY_ID from "@/constants/directory-id";
import DUMMY_IMAGE from "@/constants/dummy-image-value";
import { useAuth } from "@/hooks/use-auth";
import { apiFileDelete } from "@/service/api-client/api-file";
import {
apiGetOnePortofolio,
apiUpdatePortofolio,
} from "@/service/api-client/api-portofolio";
import { uploadImageService } from "@/service/upload-service";
import pickImage from "@/utils/pickImage";
import { Image } from "expo-image";
import { router, useLocalSearchParams } from "expo-router"; import { router, useLocalSearchParams } from "expo-router";
import { useEffect, useState } from "react";
import Toast from "react-native-toast-message";
export default function PortofolioEditLogo() { export default function PortofolioEditLogo() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const [logoId, setLogoId] = useState<any>();
const [imageUri, setImageUri] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const { token } = useAuth();
useEffect(() => {
onLoadData(id as string);
}, [id]);
const onLoadData = async (id: string) => {
const response = await apiGetOnePortofolio({ id: id });
console.log(
"Response portofolio >>",
JSON.stringify(response.data.logoId, null, 2)
);
setLogoId(response.data.logoId);
};
async function onUpload() {
try {
setIsLoading(true);
const response = await uploadImageService({
imageUri,
dirId: DIRECTORY_ID.portofolio_logo,
});
if (response.success) {
const fileId = response.data.id;
const responseUpdate = await apiUpdatePortofolio({
id: id as string,
data: { fileId },
category: "logo",
});
if (!responseUpdate.success) {
Toast.show({
type: "error",
text1: "Info",
text2: responseUpdate.message,
});
return;
}
if (logoId) {
const deletePrevFile = await apiFileDelete({
token: token as string,
id: logoId as string,
});
if (!deletePrevFile.success) {
console.log("error delete prev file >>", deletePrevFile.message);
}
}
Toast.show({
type: "success",
text1: "Sukses",
text2: "Logo berhasil diupdate",
});
router.back();
}
} catch (error) {
Toast.show({
type: "error",
text1: "Gagal",
text2: error as string,
});
} finally {
setIsLoading(false);
}
}
const image = imageUri ? (
<Image source={{ uri: imageUri }} style={{ width: 200, height: 200 }} />
) : (
<Image
source={
logoId
? { uri: API_STRORAGE.GET({ fileId: logoId }) }
: DUMMY_IMAGE.avatar
}
style={{ width: 200, height: 200 }}
/>
);
const buttonFooter = ( const buttonFooter = (
<BoxButtonOnFooter> <BoxButtonOnFooter>
<ButtonCustom <ButtonCustom
isLoading={isLoading}
disabled={isLoading}
onPress={() => { onPress={() => {
console.log("Simpan logo "); onUpload();
router.back();
}} }}
> >
Simpan Update
</ButtonCustom> </ButtonCustom>
</BoxButtonOnFooter> </BoxButtonOnFooter>
); );
@@ -34,13 +134,17 @@ export default function PortofolioEditLogo() {
height: 250, height: 250,
}} }}
> >
<AvatarCustom size="xl" /> {image}
</BaseBox> </BaseBox>
<ButtonCenteredOnly <ButtonCenteredOnly
icon="upload" icon="upload"
onPress={() => router.navigate(`/take-picture/${id}`)} onPress={() => {
pickImage({
setImageUri,
});
}}
> >
Update Upload
</ButtonCenteredOnly> </ButtonCenteredOnly>
</ViewWrapper> </ViewWrapper>
</> </>

View File

@@ -4,20 +4,87 @@ import {
TextInputCustom, TextInputCustom,
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import {
apiGetOnePortofolio,
apiUpdatePortofolio,
} from "@/service/api-client/api-portofolio";
import { useLocalSearchParams, router } from "expo-router"; import { useLocalSearchParams, router } from "expo-router";
import { useEffect, useState } from "react";
import Toast from "react-native-toast-message";
export default function PortofolioEditSocialMedia() { export default function PortofolioEditSocialMedia() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
console.log("ID >>", id);
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState<any>({
facebook: "",
twitter: "",
instagram: "",
tiktok: "",
youtube: "",
});
useEffect(() => {
onLoadData(id as string);
}, [id]);
const onLoadData = async (id: string) => {
const response = await apiGetOnePortofolio({ id: id });
console.log(
"Response portofolio >>",
JSON.stringify(response.data.Portofolio_MediaSosial, null, 2)
);
const data = response.data.Portofolio_MediaSosial;
setData({
facebook: data.facebook,
twitter: data.twitter,
instagram: data.instagram,
tiktok: data.tiktok,
youtube: data.youtube,
});
};
const onSubmitUpdate = async () => {
try {
setIsLoading(true);
const response = await apiUpdatePortofolio({
id: id as string,
data: data,
category: "medsos",
});
if (!response.success) {
Toast.show({
type: "info",
text1: "Info",
text2: response.message,
});
return;
}
Toast.show({
type: "success",
text1: "Sukses",
text2: "Data media terupdate",
});
router.back();
} catch (error) {
console.log("Error onSubmitUpdate", error);
} finally {
setIsLoading(false);
}
};
const buttonFooter = ( const buttonFooter = (
<BoxButtonOnFooter> <BoxButtonOnFooter>
<ButtonCustom <ButtonCustom
onPress={() => { isLoading={isLoading}
console.log(`Simpan sosmed ${id}`); disabled={isLoading}
router.back(); onPress={onSubmitUpdate}
}}
> >
Simpan Update
</ButtonCustom> </ButtonCustom>
</BoxButtonOnFooter> </BoxButtonOnFooter>
); );
@@ -25,11 +92,36 @@ export default function PortofolioEditSocialMedia() {
return ( return (
<> <>
<ViewWrapper footerComponent={buttonFooter}> <ViewWrapper footerComponent={buttonFooter}>
<TextInputCustom label="Tiktok" placeholder="Masukkan tiktok" /> <TextInputCustom
<TextInputCustom label="Instagram" placeholder="Masukkan instagram" /> value={data.tiktok}
<TextInputCustom label="Facebook" placeholder="Masukkan facebook" /> onChangeText={(value) => setData({ ...data, tiktok: value })}
<TextInputCustom label="Twitter" placeholder="Masukkan twitter" /> label="Tiktok"
<TextInputCustom label="Youtube" placeholder="Masukkan youtube" /> placeholder="Masukkan tiktok"
/>
<TextInputCustom
value={data.instagram}
onChangeText={(value) => setData({ ...data, instagram: value })}
label="Instagram"
placeholder="Masukkan instagram"
/>
<TextInputCustom
value={data.facebook}
onChangeText={(value) => setData({ ...data, facebook: value })}
label="Facebook"
placeholder="Masukkan facebook"
/>
<TextInputCustom
value={data.twitter}
onChangeText={(value) => setData({ ...data, twitter: value })}
label="Twitter"
placeholder="Masukkan twitter"
/>
<TextInputCustom
value={data.youtube}
onChangeText={(value) => setData({ ...data, youtube: value })}
label="Youtube"
placeholder="Masukkan youtube"
/>
</ViewWrapper> </ViewWrapper>
</> </>
); );

View File

@@ -0,0 +1,357 @@
import {
ActionIcon,
BoxButtonOnFooter,
ButtonCustom,
CenterCustom,
SelectCustom,
Spacing,
StackCustom,
TextAreaCustom,
TextCustom,
TextInputCustom,
ViewWrapper,
} from "@/components";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
import {
apiMasterBidangBisnis,
apiMasterSubBidangBisnis,
} from "@/service/api-client/api-master";
import {
apiGetOnePortofolio,
apiUpdatePortofolio,
} from "@/service/api-client/api-portofolio";
import {
IMasterBidangBisnis,
IMasterSubBidangBisnis,
} from "@/types/Type-Master";
import { Ionicons } from "@expo/vector-icons";
import { router, useLocalSearchParams } from "expo-router";
import _ from "lodash";
import { useEffect, useState } from "react";
import { Text, View } from "react-native";
import PhoneInput, { ICountry } from "react-native-international-phone-number";
import Toast from "react-native-toast-message";
interface IFormData {
id_Portofolio: string;
namaBisnis: string;
alamatKantor: string;
tlpn: string;
deskripsi: string;
masterBidangBisnisId: string;
subBidang: any[];
}
interface IListSubBidangSelected {
id: string;
MasterSubBidangBisnis: {
id: string;
name: string;
masterBidangBisnisId: string;
};
}
export default function PortofolioEdit() {
const { id } = useLocalSearchParams();
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState<any>({});
const [selectedCountry, setSelectedCountry] = useState<null | ICountry>(null);
const [bidangBisnis, setBidangBisnis] = useState<IMasterBidangBisnis[]>([]);
const [subBidangBisnis, setSubBidangBisnis] = useState<
IMasterSubBidangBisnis[]
>([]);
const [selectedSubBidang, setSelectedSubBidang] = useState<string[]>([]);
const [listSubBidangSelected, setListSubBidangSelected] = useState<
IListSubBidangSelected[]
>([
{
id: "",
MasterSubBidangBisnis: {
id: "",
name: "",
masterBidangBisnisId: "",
},
},
]);
function handleInputValue(phoneNumber: string) {
setData({ ...data, tlpn: phoneNumber });
}
function handleSelectedCountry(country: ICountry) {
setSelectedCountry(country);
}
useEffect(() => {
onLoadData(id as string);
onLoadMasterBidang();
onLoadMasterSubBidangBisnis();
}, [id]);
const onLoadData = async (id: string) => {
const response = await apiGetOnePortofolio({ id: id });
if (response.data.tlpn && response.data.tlpn.includes("62")) {
const fixNumber = response.data.tlpn.replace("62", "");
setData({ ...response.data, tlpn: fixNumber });
}
};
const onLoadMasterBidang = async () => {
try {
const response = await apiMasterBidangBisnis();
setBidangBisnis(response.data);
} catch (error) {
setBidangBisnis([]);
console.log("Error onLoadMasterBidangBisnis", error);
}
};
async function onLoadMasterSubBidangBisnis() {
try {
const response = await apiMasterSubBidangBisnis({});
if (response.success) {
setSubBidangBisnis(response.data);
}
} catch (error) {
console.error("Error on load master sub bidang bisnis", error);
}
}
const handleSubmitUpdate = async () => {
try {
setIsLoading(true);
const callingCode = selectedCountry?.callingCode.replace(/^\+/, "") || "";
const fixNumber = data.tlpn.replace(/\s+/g, "");
const realNumber = callingCode + fixNumber;
const newData: IFormData = {
id_Portofolio: data.id_Portofolio,
namaBisnis: data.namaBisnis,
alamatKantor: data.alamatKantor,
tlpn: realNumber,
deskripsi: data.deskripsi,
masterBidangBisnisId: data.masterBidangBisnisId,
subBidang: listSubBidangSelected,
};
const response = await apiUpdatePortofolio({
id: id as string,
data: newData,
category: "detail",
});
if (!response.success) {
Toast.show({
type: "info",
text1: "Info",
text2: response.message,
});
return;
}
Toast.show({
type: "success",
text1: "Sukses",
text2: "Data terupdate",
});
router.back();
} catch (error) {
console.log("Error handleSubmitUpdate", error);
} finally {
setIsLoading(false);
}
};
const buttonUpdate = (
<BoxButtonOnFooter>
<ButtonCustom
isLoading={isLoading}
disabled={isLoading}
onPress={handleSubmitUpdate}
>
Update
</ButtonCustom>
</BoxButtonOnFooter>
);
return (
<>
<ViewWrapper footerComponent={buttonUpdate}>
<StackCustom gap={"xs"}>
<TextInputCustom
required
label="Nama Bisnis"
placeholder="Masukkan nama bisnis"
value={data.namaBisnis}
onChangeText={(value: any) =>
setData({ ...data, namaBisnis: value })
}
/>
<SelectCustom
label="Bidang Usaha"
required
data={bidangBisnis?.map((item) => ({
label: item.name,
value: item.id,
}))}
value={data.masterBidangBisnisId}
onChange={(value) => {
setData({ ...(data as any), masterBidangBisnisId: value });
}}
/>
{listSubBidangSelected.map((item, index) => (
<SelectCustom
key={index}
label="Sub Bidang Usaha"
required
data={subBidangBisnis?.map((item) => ({
label: item.name,
value: item.id,
}))}
value={item.id || null}
onChange={(value) => {
console.log("Value >>", value);
}}
/>
))}
<CenterCustom>
<View
style={{ flexDirection: "row", alignItems: "center", gap: 10 }}
>
<ActionIcon
// disabled={
// selectedSubBidang.length === listSubBidangSelected.length
// }
onPress={() => {
setListSubBidangSelected([
...listSubBidangSelected,
{
id: "",
MasterSubBidangBisnis: {
id: "",
name: "",
masterBidangBisnisId: "",
},
},
]);
}}
icon={
<Ionicons
name="add-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
<ActionIcon
// disabled={listSubBidangSelected.length <= 1}
onPress={() => {
const list = _.clone(listSubBidangSelected);
list.pop();
setListSubBidangSelected(list);
}}
icon={
<Ionicons
name="remove-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
</View>
</CenterCustom>
<Spacing />
{/* <Grid>
<Grid.Col span={10}>
<SelectCustom
// disabled
label="Sub Bidang Usaha"
required
data={dummyMasterSubBidangBisnis.map((item) => ({
label: item.name,
value: item.id,
}))}
value={data.masterSubBidangBisnisId}
onChange={(value) => {
setData({ ...(data as any), masterSubBidangBisnisId: value });
}}
/>
</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")}>
Tambah Pilihan
</ButtonCenteredOnly>
<Spacing /> */}
<View>
<View style={{ flexDirection: "row", alignItems: "center" }}>
<TextCustom semiBold style={{ color: MainColor.white_gray }}>
Nomor Telepon
</TextCustom>
<Text style={{ color: "red" }}> *</Text>
</View>
<Spacing height={5} />
<PhoneInput
value={data.tlpn}
onChangePhoneNumber={handleInputValue}
selectedCountry={selectedCountry}
onChangeSelectedCountry={handleSelectedCountry}
defaultCountry="ID"
placeholder="xxx-xxx-xxx"
/>
</View>
<Spacing />
<TextInputCustom
required
label="Alamat Bisnis"
placeholder="Masukkan alamat bisnis"
value={data.alamatKantor}
onChangeText={(value: any) =>
setData({ ...data, alamatKantor: value })
}
/>
<TextAreaCustom
label="Deskripsi Bisnis"
placeholder="Masukkan deskripsi bisnis"
value={data.deskripsi}
onChangeText={(value: any) =>
setData({ ...data, deskripsi: value })
}
autosize
minRows={2}
maxRows={5}
required
showCount
maxLength={1000}
/>
<Spacing />
<TextCustom>{JSON.stringify(subBidangBisnis, null, 2)}</TextCustom>
</StackCustom>
</ViewWrapper>
</>
);
}

View File

@@ -1,8 +1,9 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
ActionIcon,
BoxButtonOnFooter, BoxButtonOnFooter,
ButtonCenteredOnly,
ButtonCustom, ButtonCustom,
Grid, CenterCustom,
SelectCustom, SelectCustom,
Spacing, Spacing,
StackCustom, StackCustom,
@@ -12,46 +13,319 @@ import {
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import dummyMasterBidangBisnis from "@/lib/dummy-data/master-bidang-bisnis"; import { ICON_SIZE_XLARGE } from "@/constants/constans-value";
import dummyMasterSubBidangBisnis from "@/lib/dummy-data/master-sub-bidang-bisnis"; import {
apiMasterBidangBisnis,
apiMasterSubBidangBisnis,
} from "@/service/api-client/api-master";
import {
apiGetOnePortofolio,
apiUpdatePortofolio,
} from "@/service/api-client/api-portofolio";
import {
IMasterBidangBisnis,
IMasterSubBidangBisnis,
} from "@/types/Type-Master";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { router, useLocalSearchParams } from "expo-router"; import { router, useLocalSearchParams } from "expo-router";
import { useState } from "react"; import _ from "lodash";
import { Text, TouchableOpacity, View } from "react-native"; import { useEffect, useState } from "react";
import { Text, View } from "react-native";
import PhoneInput, { ICountry } from "react-native-international-phone-number"; import PhoneInput, { ICountry } from "react-native-international-phone-number";
import { ActivityIndicator } from "react-native-paper";
import Toast from "react-native-toast-message";
interface IFormData {
id_Portofolio: string;
namaBisnis: string;
alamatKantor: string;
tlpn: string;
deskripsi: string;
masterBidangBisnisId: string;
subBidang: any[];
}
interface IListSubBidangSelected {
id: string;
MasterSubBidangBisnis?: {
id?: string;
name?: string;
masterBidangBisnisId?: string;
};
}
export default function PortofolioEdit() { export default function PortofolioEdit() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const [selectedCountry, setSelectedCountry] = useState<null | ICountry>(null); const [isLoading, setIsLoading] = useState(false);
const [inputValue, setInputValue] = useState<string>(""); const [data, setData] = useState<any>({});
const [data, setData] = useState({ const [selectedCountry, setSelectedCountry] = useState<null | ICountry>(null);
name: "", const [bidangBisnis, setBidangBisnis] = useState<
bidang_usaha: "", IMasterBidangBisnis[] | null
sub_bidang_usaha: "", >(null);
alamat: "", const [subBidangBisnis, setSubBidangBisnis] = useState<
nomor_telepon: "", IMasterSubBidangBisnis[] | null
deskripsi: "", >(null);
}); const [selectedSubBidang, setSelectedSubBidang] = useState<string[]>([]);
const [listSubBidangSelected, setListSubBidangSelected] = useState<
IListSubBidangSelected[]
>([]);
function handleInputValue(phoneNumber: string) { function handleInputValue(phoneNumber: string) {
setInputValue(phoneNumber); setData({ ...data, tlpn: phoneNumber });
} }
function handleSelectedCountry(country: ICountry) { function handleSelectedCountry(country: ICountry) {
setSelectedCountry(country); setSelectedCountry(country);
} }
function handleSave() { const onLoadMasterBidang = async () => {
console.log(`Update portofolio berhasil ${id}`); try {
router.back(); const response = await apiMasterBidangBisnis();
setBidangBisnis(response.data);
return response.success;
} catch (error) {
setBidangBisnis([]);
console.log("Error onLoadMasterBidangBisnis", error);
}
};
async function onLoadMasterSubBidangBisnis() {
try {
const response = await apiMasterSubBidangBisnis({});
setSubBidangBisnis(response.data);
return response.success;
} catch (error) {
console.error("Error on load master sub bidang bisnis", error);
}
} }
const handleLoadMaster = async (id: string) => {
const loadBidang = await onLoadMasterBidang();
const loadSubBidang = await onLoadMasterSubBidangBisnis();
if (!loadBidang || !loadSubBidang) {
return;
}
onLoadData(id);
};
useEffect(() => {
handleLoadMaster(id as any);
}, [id]);
const onLoadData = async (id: string) => {
const response = await apiGetOnePortofolio({ id: id });
if (response.success) {
const fixNumber = response.data.tlpn.replace("62", "");
setData({ ...response.data, tlpn: fixNumber });
// Cek apakah ada sub bidang bisnis yang terpilih
const prevSubBidang = response.data.Portofolio_BidangDanSubBidangBisnis;
if (prevSubBidang && prevSubBidang.length > 0) {
setListSubBidangSelected(prevSubBidang);
} else {
// Jika tidak ada sub bidang yang terpilih sebelumnya, tetap inisialisasi dengan array kosong
setListSubBidangSelected([
{
id: "",
MasterSubBidangBisnis: {
id: "",
name: "",
},
},
]);
}
const bisnisId = response.data.masterBidangBisnisId;
handleLoadSelectedSubBidang({
id: bisnisId,
});
}
};
// Handler untuk saat komponen pertama kali load
const handleLoadSelectedSubBidang = ({ id }: { id: string }) => {
if (!subBidangBisnis) return;
const filteredSubBidang: any = subBidangBisnis.filter((item) => {
return item.masterBidangBisnisId === id;
});
setSelectedSubBidang(filteredSubBidang);
};
// Handler untuk menambah sub bidang bisnis
const handleAddSubBidang = () => {
setListSubBidangSelected([
...listSubBidangSelected,
{
id: "",
MasterSubBidangBisnis: { id: "", name: "" },
},
]);
};
// Handler untuk menghapus sub bidang bisnis
const handleRemoveSubBidang = (index: number) => {
if (listSubBidangSelected.length <= 1) return;
const updatedList = [...listSubBidangSelected];
updatedList.splice(index, 1);
setListSubBidangSelected(updatedList);
};
// Handler untuk perubahan bidang bisnis
const handleBidangBisnisChange = (val: string) => {
const isSameBidang = data?.MasterBidangBisnis?.id === val;
setData({ ...(data as any), masterBidangBisnisId: val });
// Reset sub bidang jika ganti bidang
if (!isSameBidang) {
setListSubBidangSelected([
{
id: "",
MasterSubBidangBisnis: { id: "", name: "" },
},
]);
}
handleLoadSelectedSubBidang({ id: val });
};
// Handler untuk update sub bidang
const handleSubBidangChange = (value: string, index: number) => {
const select = selectedSubBidang.find((sub: any) => sub.id === value);
const list: any = _.cloneDeep(listSubBidangSelected);
list[index] = {
id: "",
MasterSubBidangBisnis: select || {
id: value,
name: "",
masterBidangBisnisId: "",
},
};
setListSubBidangSelected(list);
};
useEffect(() => {
if (subBidangBisnis?.length !== undefined && data.masterBidangBisnisId) {
handleLoadSelectedSubBidang({
id: data.masterBidangBisnisId,
});
}
}, [subBidangBisnis, data.masterBidangBisnisId]);
function validateData(data: any) {
if (
!data.namaBisnis ||
!data.alamatKantor ||
!data.tlpn ||
!data.deskripsi ||
!data.masterBidangBisnisId
) {
return false;
}
return true;
}
function validateDataSubBidang(dataArray: any[]) {
return !dataArray.some(
(item: any) =>
!item.MasterSubBidangBisnis.id ||
item.MasterSubBidangBisnis.id.trim() === ""
);
}
const handleSubmitUpdate = async () => {
const callingCode = selectedCountry?.callingCode.replace(/^\+/, "") || "";
const fixNumber = data.tlpn.replace(/\s+/g, "");
const realNumber = callingCode + fixNumber;
const newData: IFormData = {
id_Portofolio: data.id_Portofolio,
namaBisnis: data.namaBisnis,
alamatKantor: data.alamatKantor,
tlpn: realNumber,
deskripsi: data.deskripsi,
masterBidangBisnisId: data.masterBidangBisnisId,
subBidang: listSubBidangSelected,
};
if (!validateData(newData)) {
return Toast.show({
type: "error",
text1: "Harap lengkapi data",
});
}
if (!validateDataSubBidang(listSubBidangSelected as any)) {
return Toast.show({
type: "error",
text1: "Harap lengkapi sub bidang",
});
}
try {
setIsLoading(true);
const response = await apiUpdatePortofolio({
id: id as string,
data: newData,
category: "detail",
});
if (!response.success) {
Toast.show({
type: "info",
text1: "Info",
text2: response.message,
});
return;
}
Toast.show({
type: "success",
text1: "Sukses",
text2: "Data terupdate",
});
router.back();
} catch (error) {
console.log("Error handleSubmitUpdate", error);
} finally {
setIsLoading(false);
}
};
const buttonUpdate = ( const buttonUpdate = (
<BoxButtonOnFooter> <BoxButtonOnFooter>
<ButtonCustom onPress={handleSave}>Simpan</ButtonCustom> <ButtonCustom
isLoading={isLoading}
disabled={isLoading}
onPress={handleSubmitUpdate}
>
Update
</ButtonCustom>
</BoxButtonOnFooter> </BoxButtonOnFooter>
); );
if (!bidangBisnis || !subBidangBisnis) {
return (
<>
<ViewWrapper>
<ActivityIndicator size="large" color={MainColor.yellow} />
</ViewWrapper>
</>
);
}
return ( return (
<> <>
<ViewWrapper footerComponent={buttonUpdate}> <ViewWrapper footerComponent={buttonUpdate}>
@@ -60,50 +334,98 @@ export default function PortofolioEdit() {
required required
label="Nama Bisnis" label="Nama Bisnis"
placeholder="Masukkan nama bisnis" placeholder="Masukkan nama bisnis"
value={data.namaBisnis}
onChangeText={(value: any) =>
setData({ ...data, namaBisnis: value })
}
/> />
<SelectCustom <SelectCustom
label="Bidang Usaha" label="Bidang Usaha"
required required
data={dummyMasterBidangBisnis.map((item) => ({ data={bidangBisnis?.map((item) => ({
label: item.name, label: item.name,
value: item.id, value: item.id,
}))} }))}
value={data.bidang_usaha} value={data.masterBidangBisnisId}
onChange={(value) => { onChange={(value: any) => {
setData({ ...(data as any), bidang_usaha: value }); handleBidangBisnisChange(value);
}} }}
/> />
<Grid> {listSubBidangSelected.map((item, index) => {
<Grid.Col span={10}> // Filter data untuk select sub bidang, menghilangkan yang sudah dipilih kecuali untuk item ini sendiri
const selectedIds = listSubBidangSelected
.filter((_, i) => i !== index)
.map((s) => s.MasterSubBidangBisnis?.id)
.filter((id) => id); // Filter hanya yang memiliki id (tidak kosong)
const availableSubBidangOptions = (selectedSubBidang || [])
.filter((sub: any) => {
// Tampilkan jika ini adalah opsi yang dipilih saat ini atau belum dipilih di sub bidang lainnya
return (
sub.id === item.MasterSubBidangBisnis?.id ||
!selectedIds.includes(sub.id)
);
})
.map((sub: any) => ({
value: sub.id,
label: sub.name,
}));
return (
<SelectCustom <SelectCustom
// disabled key={index}
label="Sub Bidang Usaha" label="Sub Bidang Usaha"
required required
data={dummyMasterSubBidangBisnis.map((item) => ({ data={availableSubBidangOptions}
label: item.name, value={item.MasterSubBidangBisnis?.id || null}
value: item.id, onChange={(value: any) => {
}))} handleSubBidangChange(value, index);
value={data.sub_bidang_usaha}
onChange={(value) => {
setData({ ...(data as any), sub_bidang_usaha: value });
}} }}
/> />
</Grid.Col> );
<Grid.Col })}
span={2}
style={{ alignItems: "center", justifyContent: "center" }} <CenterCustom>
<View
style={{ flexDirection: "row", alignItems: "center", gap: 10 }}
> >
<TouchableOpacity onPress={() => console.log("delete")}> <ActionIcon
<Ionicons name="trash" size={24} color={MainColor.red} /> disabled={
</TouchableOpacity> selectedSubBidang.length === listSubBidangSelected.length
</Grid.Col> }
</Grid> onPress={() => {
<ButtonCenteredOnly onPress={() => console.log("add")}> handleAddSubBidang();
Tambah Pilihan }}
</ButtonCenteredOnly> icon={
<Ionicons
name="add-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
<ActionIcon
disabled={listSubBidangSelected.length <= 1}
onPress={() => {
handleRemoveSubBidang(listSubBidangSelected.length - 1);
}}
icon={
<Ionicons
name="remove-circle-outline"
size={ICON_SIZE_XLARGE}
color={MainColor.black}
/>
}
size="xl"
/>
</View>
</CenterCustom>
<Spacing /> <Spacing />
<View> <View>
<View style={{ flexDirection: "row", alignItems: "center" }}> <View style={{ flexDirection: "row", alignItems: "center" }}>
<TextCustom semiBold style={{ color: MainColor.white_gray }}> <TextCustom semiBold style={{ color: MainColor.white_gray }}>
@@ -113,7 +435,7 @@ export default function PortofolioEdit() {
</View> </View>
<Spacing height={5} /> <Spacing height={5} />
<PhoneInput <PhoneInput
value={inputValue} value={data.tlpn}
onChangePhoneNumber={handleInputValue} onChangePhoneNumber={handleInputValue}
selectedCountry={selectedCountry} selectedCountry={selectedCountry}
onChangeSelectedCountry={handleSelectedCountry} onChangeSelectedCountry={handleSelectedCountry}
@@ -127,6 +449,10 @@ export default function PortofolioEdit() {
required required
label="Alamat Bisnis" label="Alamat Bisnis"
placeholder="Masukkan alamat bisnis" placeholder="Masukkan alamat bisnis"
value={data.alamatKantor}
onChangeText={(value: any) =>
setData({ ...data, alamatKantor: value })
}
/> />
<TextAreaCustom <TextAreaCustom
@@ -141,7 +467,7 @@ export default function PortofolioEdit() {
maxRows={5} maxRows={5}
required required
showCount showCount
maxLength={100} maxLength={1000}
/> />
<Spacing /> <Spacing />
</StackCustom> </StackCustom>

View File

@@ -1,20 +1,31 @@
import { AlertCustom, DrawerCustom } from "@/components"; /* eslint-disable react-hooks/exhaustive-deps */
import { DrawerCustom, LoaderCustom, Spacing, StackCustom } from "@/components";
import LeftButtonCustom from "@/components/Button/BackButton"; import LeftButtonCustom from "@/components/Button/BackButton";
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 Portofolio_BusinessLocation from "@/screens/Portofolio/BusinessLocationSection";
import Portofolio_ButtonDelete from "@/screens/Portofolio/ButtonDelete";
import Portofolio_Data from "@/screens/Portofolio/DataPortofolio";
import { drawerItemsPortofolio } from "@/screens/Portofolio/ListPage"; import { drawerItemsPortofolio } from "@/screens/Portofolio/ListPage";
import Portofolio_MenuDrawerSection from "@/screens/Portofolio/MenuDrawer"; import Portofolio_MenuDrawerSection from "@/screens/Portofolio/MenuDrawer";
import PorfofolioSection from "@/screens/Portofolio/PorfofolioSection"; import Portofolio_SocialMediaSection from "@/screens/Portofolio/SocialMediaSection";
import { apiGetOnePortofolio } from "@/service/api-client/api-portofolio";
import { apiUser } from "@/service/api-client/api-user";
import { GStyles } from "@/styles/global-styles"; import { GStyles } from "@/styles/global-styles";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { Stack, useLocalSearchParams, router } from "expo-router"; import { Stack, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useState } from "react"; import { useCallback, useState } from "react";
import { TouchableOpacity } from "react-native"; import { TouchableOpacity } from "react-native";
export default function Portofolio() { export default function Portofolio() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [deleteAlert, setDeleteAlert] = useState(false); const [isLoadingDelete, setIsLoadingDelete] = useState(false);
const [data, setData] = useState<any>();
const [profileId, setProfileId] = useState<any>();
const { user } = useAuth();
const openDrawer = () => { const openDrawer = () => {
setIsDrawerOpen(true); setIsDrawerOpen(true);
@@ -22,15 +33,41 @@ export default function Portofolio() {
const closeDrawer = () => { const closeDrawer = () => {
setIsDrawerOpen(false); setIsDrawerOpen(false);
}; };
useFocusEffect(
useCallback(() => {
onLoadData(id as string);
onLoadUserByToken();
}, [id])
);
async function onLoadData(id: string) {
const response = await apiGetOnePortofolio({ id: id });
console.log(
"[PROFILE ID]>>",
JSON.stringify(response.data.Profile.id, null, 2)
);
setData(response.data);
}
const onLoadUserByToken = async () => {
const response = await apiUser(user?.id as string);
console.log(
"[PROFILE LOGIN]>>",
JSON.stringify(response.data?.Profile.id, null, 2)
);
setProfileId(response?.data?.Profile?.id);
};
return ( return (
<> <>
<ViewWrapper> {/* Header */}
{/* Header */} <Stack.Screen
<Stack.Screen options={{
options={{ title: "Portofolio",
title: "Portofolio", headerLeft: () => <LeftButtonCustom />,
headerLeft: () => <LeftButtonCustom />, headerRight: () =>
headerRight: () => ( data?.Profile?.id !== profileId ? null : (
<TouchableOpacity onPress={openDrawer}> <TouchableOpacity onPress={openDrawer}>
<Ionicons <Ionicons
name="ellipsis-vertical" name="ellipsis-vertical"
@@ -39,40 +76,44 @@ export default function Portofolio() {
/> />
</TouchableOpacity> </TouchableOpacity>
), ),
headerStyle: GStyles.headerStyle, headerStyle: GStyles.headerStyle,
headerTitleStyle: GStyles.headerTitleStyle, headerTitleStyle: GStyles.headerTitleStyle,
}} }}
/> />
<PorfofolioSection setShowDeleteAlert={setDeleteAlert} /> <ViewWrapper>
{!data || !profileId ? (
<LoaderCustom />
) : (
<StackCustom>
<Portofolio_Data
data={data}
listSubBidang={data?.Portofolio_BidangDanSubBidangBisnis as any[]}
/>
<Portofolio_BusinessLocation />
<Portofolio_SocialMediaSection
data={data?.Portofolio_MediaSosial}
/>
<Portofolio_ButtonDelete
id={id as string}
isLoadingDelete={isLoadingDelete}
setIsLoadingDelete={setIsLoadingDelete}
/>
<Spacing />
</StackCustom>
)}
</ViewWrapper> </ViewWrapper>
{/* Drawer Komponen Eksternal */} {/* Drawer Komponen Eksternal */}
<DrawerCustom <DrawerCustom
isVisible={isDrawerOpen} isVisible={isDrawerOpen}
closeDrawer={closeDrawer} closeDrawer={closeDrawer}
height={350} height={"auto"}
> >
<Portofolio_MenuDrawerSection <Portofolio_MenuDrawerSection
drawerItems={drawerItemsPortofolio({ id: id as string })} drawerItems={drawerItemsPortofolio({ id: id as string })}
setIsDrawerOpen={setIsDrawerOpen} setIsDrawerOpen={setIsDrawerOpen}
/> />
</DrawerCustom> </DrawerCustom>
{/* Alert Delete */}
<AlertCustom
isVisible={deleteAlert}
onLeftPress={() => setDeleteAlert(false)}
onRightPress={() => {
setDeleteAlert(false);
console.log("Hapus portofolio");
router.back();
}}
title="Hapus Portofolio"
message="Apakah Anda yakin ingin menghapus portofolio ini?"
textLeft="Batal"
textRight="Hapus"
colorRight={MainColor.red}
/>
</> </>
); );
} }

View File

@@ -1,47 +1,28 @@
import { BaseBox, Grid, TextCustom, ViewWrapper } from "@/components"; import { TextCustom, ViewWrapper } from "@/components";
import { MainColor } from "@/constants/color-palet"; import Portofolio_BoxView from "@/screens/Portofolio/BoxPortofolioView";
import { ICON_SIZE_SMALL } from "@/constants/constans-value"; import { apiGetPortofolio } from "@/service/api-client/api-portofolio";
import { Ionicons } from "@expo/vector-icons"; import { useFocusEffect, useLocalSearchParams } from "expo-router";
import { router, useLocalSearchParams } from "expo-router"; import { useCallback, useState } from "react";
export default function ListPortofolio() { export default function ListPortofolio() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const [data, setData] = useState<any[]>([]);
useFocusEffect(
useCallback(() => {
onLoadPortofolio(id as string);
}, [id])
);
const onLoadPortofolio = async (id: string) => {
const response = await apiGetPortofolio({ id: id });
setData(response.data);
};
return ( return (
<ViewWrapper> <ViewWrapper>
{Array.from({ length: 10 }).map((_, index) => ( {data ? data?.map((item: any, index: number) => (
<BaseBox <Portofolio_BoxView key={index} data={item} />
key={index} )) : <TextCustom>Tidak ada portofolio</TextCustom>}
style={{ backgroundColor: MainColor.darkblue }}
onPress={() => {
console.log("press to Portofolio");
router.push(`/portofolio/${id}`);
}}
>
<Grid>
<Grid.Col
span={10}
style={{ justifyContent: "center", backgroundColor: "" }}
>
<TextCustom bold size="large" truncate={1}>
Nama usaha portofolio
</TextCustom>
<TextCustom size="small" color="yellow">
#id-porofolio12345
</TextCustom>
</Grid.Col>
<Grid.Col
span={2}
style={{ alignItems: "flex-end", justifyContent: "center" }}
>
<Ionicons
name="caret-forward"
size={ICON_SIZE_SMALL}
color="white"
/>
</Grid.Col>
</Grid>
</BaseBox>
))}
</ViewWrapper> </ViewWrapper>
); );
} }

View File

@@ -39,7 +39,11 @@ export default function ProfileEdit() {
const handleUpdate = async () => { const handleUpdate = async () => {
try { try {
setIsLoading(true); setIsLoading(true);
const response = await apiUpdateProfile({ id: id as string, data }); const response = await apiUpdateProfile({
id: id as string,
data,
category: "profile",
});
if (!response.success) { if (!response.success) {
Toast.show({ Toast.show({
type: "info", type: "info",
@@ -50,6 +54,8 @@ export default function ProfileEdit() {
return; return;
} }
Toast.show({ Toast.show({
type: "success", type: "success",
text1: "Sukses", text1: "Sukses",

View File

@@ -1,3 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { LoaderCustom } from "@/components";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper"; import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import LeftButtonCustom from "@/components/Button/BackButton"; import LeftButtonCustom from "@/components/Button/BackButton";
import DrawerCustom from "@/components/Drawer/DrawerCustom"; import DrawerCustom from "@/components/Drawer/DrawerCustom";
@@ -5,16 +7,15 @@ import { MainColor } from "@/constants/color-palet";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { drawerItemsProfile } from "@/screens/Profile/ListPage"; import { drawerItemsProfile } from "@/screens/Profile/ListPage";
import Profile_MenuDrawerSection from "@/screens/Profile/menuDrawerSection"; import Profile_MenuDrawerSection from "@/screens/Profile/menuDrawerSection";
import Profile_PortofolioSection from "@/screens/Profile/PortofolioSection";
import ProfileSection from "@/screens/Profile/ProfileSection"; import ProfileSection from "@/screens/Profile/ProfileSection";
import { apiGetPortofolio } from "@/service/api-client/api-portofolio";
import { apiProfile } from "@/service/api-client/api-profile"; import { apiProfile } from "@/service/api-client/api-profile";
import { apiUser } from "@/service/api-client/api-user";
import { GStyles } from "@/styles/global-styles"; import { GStyles } from "@/styles/global-styles";
import { IProfile } from "@/types/Type-Profile"; import { IProfile } from "@/types/Type-Profile";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { import { Stack, useFocusEffect, useLocalSearchParams } from "expo-router";
Stack,
useFocusEffect,
useLocalSearchParams
} from "expo-router";
import React, { useCallback, useState } from "react"; import React, { useCallback, useState } from "react";
import { TouchableOpacity } from "react-native"; import { TouchableOpacity } from "react-native";
@@ -22,8 +23,10 @@ export default function Profile() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [data, setData] = useState<IProfile>(); const [data, setData] = useState<IProfile>();
const [dataToken, setDataToken] = useState<IProfile>();
const [listPortofolio, setListPortofolio] = useState<any[]>();
const { logout, isAdmin } = useAuth(); const { logout, isAdmin, user } = useAuth();
const openDrawer = () => { const openDrawer = () => {
setIsDrawerOpen(true); setIsDrawerOpen(true);
@@ -36,36 +39,72 @@ export default function Profile() {
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
onLoadData(id as string); onLoadData(id as string);
onLoadPortofolio(id as string);
onLoadUserByToken();
isUserCheck();
}, [id]) }, [id])
); );
async function onLoadData(id: string) { const isUserCheck = () => {
const userId = id;
const userLoginId = dataToken?.id;
return userId === userLoginId;
};
const onLoadData = async (id: string) => {
const response = await apiProfile({ id: id }); const response = await apiProfile({ id: id });
setData(response.data); setData(response.data);
} };
const onLoadUserByToken = async () => {
const response = await apiUser(user?.id as string);
setDataToken(response?.data?.Profile);
};
const onLoadPortofolio = async (id: string) => {
const response = await apiGetPortofolio({ id: id });
const lastTwoByDate = response.data
.sort(
(a: any, b: any) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
) // urut desc
.slice(0, 2);
setListPortofolio(lastTwoByDate);
};
return ( return (
<> <>
<Stack.Screen <Stack.Screen
options={{ options={{
title: "Profile", title: `Profile`,
headerLeft: () => <LeftButtonCustom />, headerLeft: () => <LeftButtonCustom />,
headerRight: () => ( headerRight: () => (
<TouchableOpacity onPress={openDrawer}> <ButtonnDot
<Ionicons id={id as string}
name="ellipsis-vertical" openDrawer={openDrawer}
size={20} isUserCheck={isUserCheck()}
color={MainColor.yellow} logout={logout}
/> />
</TouchableOpacity>
), ),
headerStyle: GStyles.headerStyle, headerStyle: GStyles.headerStyle,
headerTitleStyle: GStyles.headerTitleStyle, headerTitleStyle: GStyles.headerTitleStyle,
}} }}
/> />
{/* Main View */}
<ViewWrapper> <ViewWrapper>
{/* Header */} {!data || !dataToken ? (
<ProfileSection data={data as any} /> <LoaderCustom />
) : (
<>
<ProfileSection data={data as any} />
<Profile_PortofolioSection
data={listPortofolio as any}
profileId={id as string}
/>
</>
)}
</ViewWrapper> </ViewWrapper>
{/* Drawer Komponen Eksternal */} {/* Drawer Komponen Eksternal */}
@@ -83,3 +122,44 @@ export default function Profile() {
</> </>
); );
} }
const ButtonnDot = ({
id,
openDrawer,
isUserCheck,
logout,
}: {
id: string;
openDrawer: () => void;
isUserCheck: boolean;
logout: () => Promise<void>;
}) => {
const isId = id === undefined || id === null;
console.log("ID CHECK", id);
if (isId) {
console.log("ID UNDEFINED", id);
return (
<>
<TouchableOpacity onPress={logout}>
<Ionicons name="log-out" size={20} color={MainColor.red} />
</TouchableOpacity>
</>
);
}
return (
<>
{isUserCheck && (
<TouchableOpacity onPress={openDrawer}>
<Ionicons
name="ellipsis-vertical"
size={20}
color={MainColor.yellow}
/>
</TouchableOpacity>
)}
</>
);
};

View File

@@ -5,40 +5,142 @@ import {
ButtonCustom, ButtonCustom,
} from "@/components"; } from "@/components";
import ViewWrapper from "@/components/_ShareComponent/ViewWrapper"; import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import API_STRORAGE from "@/constants/base-url-api-strorage";
import DIRECTORY_ID from "@/constants/directory-id";
import DUMMY_IMAGE from "@/constants/dummy-image-value"; import DUMMY_IMAGE from "@/constants/dummy-image-value";
import { router, useLocalSearchParams } from "expo-router"; import { useAuth } from "@/hooks/use-auth";
import { apiFileDelete } from "@/service/api-client/api-file";
import { apiProfile, apiUpdateProfile } from "@/service/api-client/api-profile";
import { uploadImageService } from "@/service/upload-service";
import { IProfile } from "@/types/Type-Profile";
import pickImage from "@/utils/pickImage";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import { Image } from "react-native"; import { Image } from "react-native";
import Toast from "react-native-toast-message";
export default function UpdateBackgroundProfile() { export default function UpdateBackgroundProfile() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const [data, setData] = useState<IProfile>();
const [imageUri, setImageUri] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const { token } = useAuth();
useFocusEffect(
useCallback(() => {
onLoadData(id as string);
}, [id])
);
async function onLoadData(id: string) {
try {
const response = await apiProfile({ id });
setData(response.data);
} catch (error) {
console.log("error get profile >>", error);
}
}
async function onUpload() {
try {
setIsLoading(true);
const response = await uploadImageService({
imageUri,
dirId: DIRECTORY_ID.profile_background,
});
if (response.success) {
const fileId = response.data.id;
const responseUpdate = await apiUpdateProfile({
id: id as string,
data: { fileId },
category: "background",
});
if (!responseUpdate.success) {
Toast.show({
type: "error",
text1: "Info",
text2: responseUpdate.message,
});
return;
}
if (data?.imageBackgroundId) {
const deletePrevFile = await apiFileDelete({
token: token as string,
id: data?.imageBackgroundId as string,
});
if (!deletePrevFile.success) {
console.log("error delete prev file >>", deletePrevFile.message);
}
}
Toast.show({
type: "success",
text1: "Sukses",
text2: "Background berhasil diupdate",
});
router.back();
}
} catch (error) {
Toast.show({
type: "error",
text1: "Gagal",
text2: error as string,
});
} finally {
setIsLoading(false);
}
}
const buttonFooter = ( const buttonFooter = (
<BoxButtonOnFooter> <BoxButtonOnFooter>
<ButtonCustom <ButtonCustom
isLoading={isLoading}
onPress={() => { onPress={() => {
console.log("Simpan foto background >>", id); onUpload();
router.back();
}} }}
> >
Simpan Update
</ButtonCustom> </ButtonCustom>
</BoxButtonOnFooter> </BoxButtonOnFooter>
); );
const image = imageUri ? (
<Image
source={{ uri: imageUri }}
style={{ width: "100%", height: "100%" }}
/>
) : (
<Image
source={
data?.imageBackgroundId
? { uri: API_STRORAGE.GET({ fileId: data.imageBackgroundId }) }
: DUMMY_IMAGE.background
}
style={{ width: "100%", height: "100%", borderRadius: 10 }}
/>
);
return ( return (
<ViewWrapper footerComponent={buttonFooter}> <ViewWrapper footerComponent={buttonFooter}>
<BaseBox <BaseBox
style={{ alignItems: "center", justifyContent: "center", height: 250 }} style={{ alignItems: "center", justifyContent: "center", height: 250 }}
> >
<Image {image}
source={DUMMY_IMAGE.background}
resizeMode="cover"
style={{ width: "100%", height: "100%", borderRadius: 10 }}
/>
</BaseBox> </BaseBox>
<ButtonCenteredOnly <ButtonCenteredOnly
icon="upload" icon="upload"
onPress={() => router.navigate(`/(application)/take-picture/${id}`)} onPress={() => {
pickImage({
setImageUri,
});
}}
> >
Update Update
</ButtonCenteredOnly> </ButtonCenteredOnly>

View File

@@ -1,5 +1,4 @@
import { import {
AvatarCustom,
BaseBox, BaseBox,
BoxButtonOnFooter, BoxButtonOnFooter,
ButtonCenteredOnly, ButtonCenteredOnly,
@@ -9,19 +8,23 @@ import ViewWrapper from "@/components/_ShareComponent/ViewWrapper";
import API_STRORAGE from "@/constants/base-url-api-strorage"; import API_STRORAGE from "@/constants/base-url-api-strorage";
import DIRECTORY_ID from "@/constants/directory-id"; import DIRECTORY_ID from "@/constants/directory-id";
import DUMMY_IMAGE from "@/constants/dummy-image-value"; import DUMMY_IMAGE from "@/constants/dummy-image-value";
import { apiProfile } from "@/service/api-client/api-profile"; import { apiProfile, apiUpdateProfile } from "@/service/api-client/api-profile";
import { uploadImageService } from "@/service/upload-service"; import { uploadImageService } from "@/service/upload-service";
import { IProfile } from "@/types/Type-Profile"; import { IProfile } from "@/types/Type-Profile";
import pickImage from "@/utils/pickImage"; import pickImage from "@/utils/pickImage";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router"; import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { Image } from "react-native"; import { Image } from "react-native";
import Toast from "react-native-toast-message";
import { useAuth } from "@/hooks/use-auth";
import { apiFileDelete } from "@/service/api-client/api-file";
export default function UpdatePhotoProfile() { export default function UpdatePhotoProfile() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const [data, setData] = useState<IProfile>(); const [data, setData] = useState<IProfile>();
const [imageUri, setImageUri] = useState<string | null>(null); const [imageUri, setImageUri] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const { token } = useAuth();
useFocusEffect( useFocusEffect(
useCallback(() => { useCallback(() => {
@@ -32,10 +35,7 @@ export default function UpdatePhotoProfile() {
async function onLoadData(id: string) { async function onLoadData(id: string) {
try { try {
const response = await apiProfile({ id }); const response = await apiProfile({ id });
console.log(
"response image id >>",
JSON.stringify(response.data.imageId, null, 2)
);
setData(response.data); setData(response.data);
} catch (error) { } catch (error) {
console.log("error get profile >>", error); console.log("error get profile >>", error);
@@ -51,12 +51,48 @@ export default function UpdatePhotoProfile() {
dirId: DIRECTORY_ID.profile_foto, dirId: DIRECTORY_ID.profile_foto,
}); });
console.log("Upload res >>", JSON.stringify(response, null, 2));
if (response.success) { if (response.success) {
const fileId = response.data.id;
const responseUpdate = await apiUpdateProfile({
id: id as string,
data: { fileId },
category: "photo",
});
if (!responseUpdate.success) {
Toast.show({
type: "error",
text1: "Info",
text2: responseUpdate.message,
});
return;
}
if (data?.imageId) {
const deletePrevFile = await apiFileDelete({
token: token as string,
id: data?.imageId as string,
});
if (!deletePrevFile.success) {
console.log("error delete prev file >>", deletePrevFile.message);
}
}
Toast.show({
type: "success",
text1: "Sukses",
text2: "Photo berhasil diupdate",
});
router.back(); router.back();
} }
} catch (error) { } catch (error) {
console.log("error upload >>", error); Toast.show({
type: "error",
text1: "Gagal",
text2: error as string,
});
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@@ -68,8 +104,6 @@ export default function UpdatePhotoProfile() {
isLoading={isLoading} isLoading={isLoading}
onPress={() => { onPress={() => {
onUpload(); onUpload();
// console.log("Simpan foto profile >>", id);
// router.back();
}} }}
> >
Update Update
@@ -80,13 +114,13 @@ export default function UpdatePhotoProfile() {
const image = imageUri ? ( const image = imageUri ? (
<Image source={{ uri: imageUri }} style={{ width: 200, height: 200 }} /> <Image source={{ uri: imageUri }} style={{ width: 200, height: 200 }} />
) : ( ) : (
<AvatarCustom <Image
size="xl"
source={ source={
data?.imageId data?.imageId
? API_STRORAGE.GET({ fileId: data.imageId }) ? { uri: API_STRORAGE.GET({ fileId: data.imageId }) }
: DUMMY_IMAGE.avatar : DUMMY_IMAGE.avatar
} }
style={{ width: 200, height: 200 }}
/> />
); );

View File

@@ -1,5 +1,5 @@
import { import {
AvatarCustom, BaseBox,
ButtonCenteredOnly, ButtonCenteredOnly,
ButtonCustom, ButtonCustom,
SelectCustom, SelectCustom,
@@ -10,16 +10,23 @@ import {
} from "@/components"; } from "@/components";
import BoxButtonOnFooter from "@/components/Box/BoxButtonOnFooter"; import BoxButtonOnFooter from "@/components/Box/BoxButtonOnFooter";
import InformationBox from "@/components/Box/InformationBox"; import InformationBox from "@/components/Box/InformationBox";
import LandscapeFrameUploaded from "@/components/Image/LandscapeFrameUploaded"; import DIRECTORY_ID from "@/constants/directory-id";
import DUMMY_IMAGE from "@/constants/dummy-image-value";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { apiCreateProfile } from "@/service/api-client/api-profile"; import { apiCreateProfile } from "@/service/api-client/api-profile";
import { router, useFocusEffect } from "expo-router"; import { apiValidationEmail } from "@/service/api-client/api-validation";
import { useCallback, useState } from "react"; import { uploadImageService } from "@/service/upload-service";
import { View } from "react-native"; import pickImage from "@/utils/pickImage";
import { router } from "expo-router";
import { useState } from "react";
import { Image, View } from "react-native";
import { Avatar } from "react-native-paper";
import Toast from "react-native-toast-message"; import Toast from "react-native-toast-message";
export default function CreateProfile() { export default function CreateProfile() {
const { user } = useAuth(); const { user } = useAuth();
const [imagePhoto, setImagePhoto] = useState<string | null>(null);
const [imageBackground, setImageBackground] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false); const [isLoading, setIsLoading] = useState<boolean>(false);
const [data, setData] = useState({ const [data, setData] = useState({
@@ -30,17 +37,11 @@ export default function CreateProfile() {
jenisKelamin: "", jenisKelamin: "",
}); });
useFocusEffect(
useCallback(() => {
Toast.show({
type: "info",
text1: "Lengkapi Profile Anda",
text2: "Untuk menjelajahi fitur-fitur yang ada",
});
}, [])
);
const handlerSave = async () => { const handlerSave = async () => {
let IMG = {
imageId: "",
imageBackgroundId: "",
};
if (!data.name || !data.email || !data.alamat || !data.jenisKelamin) { if (!data.name || !data.email || !data.alamat || !data.jenisKelamin) {
Toast.show({ Toast.show({
type: "info", type: "info",
@@ -52,7 +53,67 @@ export default function CreateProfile() {
try { try {
setIsLoading(true); setIsLoading(true);
const response = await apiCreateProfile(data);
const responseValidateEmail = await apiValidationEmail({
email: data.email,
});
if (!responseValidateEmail.success) {
Toast.show({
type: "error",
text1: "Gagal",
text2: responseValidateEmail.message,
});
return;
}
if (imagePhoto) {
try {
const responseUploadPhoto = await uploadImageService({
imageUri: imagePhoto,
dirId: DIRECTORY_ID.profile_foto,
});
if (responseUploadPhoto.success) {
const fileIdPhoto = responseUploadPhoto.data.id;
IMG.imageId = fileIdPhoto;
}
} catch (error) {
Toast.show({
type: "error",
text1: "Gagal",
text2: error as string,
});
}
}
if (imageBackground) {
try {
const responseUploadBackground = await uploadImageService({
imageUri: imageBackground,
dirId: DIRECTORY_ID.profile_background,
});
if (responseUploadBackground.success) {
const fileIdBackground = responseUploadBackground.data.id;
IMG.imageBackgroundId = fileIdBackground;
}
} catch (error) {
Toast.show({
type: "error",
text1: "Gagal",
text2: error as string,
});
}
}
const fixData = {
...data,
...IMG,
};
const response = await apiCreateProfile(fixData);
if (response.status === 400) { if (response.status === 400) {
Toast.show({ Toast.show({
type: "error", type: "error",
@@ -67,9 +128,15 @@ export default function CreateProfile() {
text1: "Sukses", text1: "Sukses",
text2: "Profile berhasil dibuat", text2: "Profile berhasil dibuat",
}); });
router.push("/(application)/(user)/home"); router.push("/(application)/(user)/home");
return;
} catch (error) { } catch (error) {
console.log("error create profile >>", error); console.log("error create profile >>", error);
Toast.show({
type: "error",
text1: "Gagal membuat profile",
});
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@@ -92,11 +159,18 @@ export default function CreateProfile() {
<StackCustom> <StackCustom>
<InformationBox text="Upload foto profile anda." /> <InformationBox text="Upload foto profile anda." />
<View style={{ alignItems: "center" }}> <View style={{ alignItems: "center" }}>
<AvatarCustom size="xl" /> <Avatar.Image
size={100}
source={imagePhoto ? { uri: imagePhoto } : DUMMY_IMAGE.avatar}
/>
<Spacing /> <Spacing />
<ButtonCenteredOnly <ButtonCenteredOnly
icon="upload" icon="upload"
onPress={() => router.navigate(`/take-picture/${user?.id}`)} onPress={() => {
pickImage({
setImageUri: setImagePhoto,
});
}}
> >
Upload Upload
</ButtonCenteredOnly> </ButtonCenteredOnly>
@@ -106,11 +180,24 @@ export default function CreateProfile() {
<View> <View>
<InformationBox text="Upload foto latar belakang anda." /> <InformationBox text="Upload foto latar belakang anda." />
<LandscapeFrameUploaded /> <BaseBox>
<Spacing /> <Image
source={
imageBackground
? { uri: imageBackground }
: DUMMY_IMAGE.background
}
style={{ width: "100%", height: 200 }}
/>
</BaseBox>
{/* <Spacing /> */}
<ButtonCenteredOnly <ButtonCenteredOnly
icon="upload" icon="upload"
onPress={() => router.navigate(`/take-picture/${user?.id}`)} onPress={() => {
pickImage({
setImageUri: setImageBackground,
});
}}
> >
Upload Upload
</ButtonCenteredOnly> </ButtonCenteredOnly>

View File

@@ -1,7 +1,8 @@
import { import {
AvatarCustom, AvatarComp,
ClickableCustom, ClickableCustom,
Grid, Grid,
LoaderCustom,
Spacing, Spacing,
StackCustom, StackCustom,
TextCustom, TextCustom,
@@ -10,39 +11,46 @@ 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 { apiAllUser } from "@/service/api-client/api-user";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { router } from "expo-router"; import { router } from "expo-router";
import _ from "lodash";
import { useEffect, useState } from "react";
export default function UserSearch() { export default function UserSearch() {
function generateRandomPhoneNumber(index: number) { const [data, setData] = useState<any[]>([]);
let prefix; const [search, setSearch] = useState<string>("");
const [isLoadList, setIsLoadList] = useState(false);
// Menentukan prefix berdasarkan index genap atau ganjil useEffect(() => {
if (index % 2 === 0) { onLoadData(search);
const evenPrefixes = ["6288", "6289", "6281"]; }, [search]);
prefix = evenPrefixes[Math.floor(Math.random() * evenPrefixes.length)];
} else { const onLoadData = async (search: string) => {
const oddPrefixes = ["6285", "6283"]; try {
prefix = oddPrefixes[Math.floor(Math.random() * oddPrefixes.length)]; setIsLoadList(true);
const response = await apiAllUser({ search: search });
console.log("[DATA USER] >", JSON.stringify(response.data, null, 2));
setData(response.data);
} catch (error) {
console.log("Error fetching data", error);
} finally {
setIsLoadList(false);
} }
};
// Menghitung panjang sisa nomor acak (antara 10 - 12 digit) const handleSearch = (search: string) => {
const remainingLength = Math.floor(Math.random() * 3) + 10; // 10, 11, atau 12 setSearch(search);
onLoadData(search);
};
// Membuat sisa nomor acak
let randomNumber = "";
for (let i = 0; i < remainingLength; i++) {
randomNumber += Math.floor(Math.random() * 10); // Digit acak antara 0-9
}
// Menggabungkan prefix dan sisa nomor
return prefix + randomNumber;
}
return ( return (
<> <>
<ViewWrapper <ViewWrapper
headerComponent={ headerComponent={
<TextInputCustom <TextInputCustom
value={search}
onChangeText={handleSearch}
iconLeft={ iconLeft={
<Ionicons <Ionicons
name="search" name="search"
@@ -57,41 +65,48 @@ export default function UserSearch() {
} }
> >
<StackCustom> <StackCustom>
{Array.from({ length: 20 }).map((e, index) => { {isLoadList ? (
return ( <LoaderCustom />
<Grid key={index}> ) : !_.isEmpty(data) ? (
<Grid.Col span={2}> data?.map((e, index) => {
<AvatarCustom href={`/profile/${index}`}/> return (
</Grid.Col> <ClickableCustom
<Grid.Col span={9}> key={index}
<TextCustom size="large">Nama user {index}</TextCustom> onPress={() => {
<TextCustom size="small"> console.log("Ke Profile");
+{generateRandomPhoneNumber(index)} router.push(`/profile/${e?.Profile?.id}`);
</TextCustom>
</Grid.Col>
<Grid.Col
span={1}
style={{
justifyContent: "center",
alignItems: "flex-end",
}} }}
> >
<ClickableCustom <Grid>
onPress={() => { <Grid.Col span={2}>
console.log("Ke Profile"); <AvatarComp fileId={e?.Profile?.imageId} size="base" />
router.push(`/profile/${index}`); </Grid.Col>
}} <Grid.Col span={9}>
> <StackCustom gap={"sm"}>
<Ionicons <TextCustom size="large">{e?.username}</TextCustom>
name="chevron-forward" <TextCustom size="small">+{e?.nomor}</TextCustom>
size={ICON_SIZE_SMALL} </StackCustom>
color={MainColor.white} </Grid.Col>
/> <Grid.Col
</ClickableCustom> span={1}
</Grid.Col> style={{
</Grid> justifyContent: "center",
); alignItems: "flex-end",
})} }}
>
<Ionicons
name="chevron-forward"
size={ICON_SIZE_SMALL}
color={MainColor.white}
/>
</Grid.Col>
</Grid>
</ClickableCustom>
);
})
) : (
<TextCustom align="center">Tidak ditemukan</TextCustom>
)}
</StackCustom> </StackCustom>
<Spacing height={50} /> <Spacing height={50} />
</ViewWrapper> </ViewWrapper>

View File

@@ -1,23 +1,63 @@
/* 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 { apiVotingGetAll } from "@/service/api-client/api-voting";
import { router } from "expo-router"; import { router } from "expo-router";
import _ from "lodash";
import { useEffect, 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("");
useEffect(() => {
onLoadData();
}, [search]);
const onLoadData = async () => {
try {
setLoadingGetData(true);
const response = await apiVotingGetAll({ search });
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,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
AlertDefaultSystem, AlertDefaultSystem,
BackButton, BackButton,
@@ -11,13 +12,39 @@ import { IconArchive, IconContribution, IconEdit } from "@/components/_Icon";
import { IMenuDrawerItem } from "@/components/_Interface/types"; import { IMenuDrawerItem } from "@/components/_Interface/types";
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 { apiVotingGetOne } from "@/service/api-client/api-voting";
import { useState } from "react"; import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import { useCallback, useState } from "react";
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 [data, setData] = useState<any>(null);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
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 handlePressDraft = (item: IMenuDrawerItem) => { const handlePressDraft = (item: IMenuDrawerItem) => {
console.log("PATH >> ", item.path); console.log("PATH >> ", item.path);
@@ -33,7 +60,7 @@ export default function VotingDetailStatus() {
textLeft: "Batal", textLeft: "Batal",
textRight: "Ya", textRight: "Ya",
onPressRight: () => { onPressRight: () => {
console.log("Hapus"); console.log("Arsip voting");
router.back(); router.back();
}, },
}); });
@@ -57,8 +84,13 @@ export default function VotingDetailStatus() {
}} }}
/> />
<ViewWrapper> <ViewWrapper>
<Voting_BoxDetailSection /> <Voting_BoxDetailSection data={data as any} />
<Voting_ButtonStatusSection status={status as string} /> <Voting_ButtonStatusSection
isLoading={isLoading}
onSetLoading={setIsLoading}
id={id as string}
status={status as string}
/>
<Spacing /> <Spacing />
</ViewWrapper> </ViewWrapper>

View File

@@ -1,29 +1,183 @@
/* 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,
});
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 +188,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,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
AlertDefaultSystem, AlertDefaultSystem,
AvatarUsernameAndOtherComponent, AvatarUsernameAndOtherComponent,
@@ -5,20 +6,90 @@ import {
DotButton, DotButton,
DrawerCustom, DrawerCustom,
InformationBox, InformationBox,
LoaderCustom,
MenuDrawerDynamicGrid, MenuDrawerDynamicGrid,
StackCustom, StackCustom,
TextCustom,
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 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"; apiVotingCheckContribution,
apiVotingGetOne,
} from "@/service/api-client/api-voting";
import {
router,
Stack,
useFocusEffect,
useLocalSearchParams,
} from "expo-router";
import React, { useCallback, useState } from "react";
import { useAuth } from "@/hooks/use-auth";
export default function VotingDetail() { export default function VotingDetail() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const { user } = useAuth();
console.log("[ID]", id);
const dateNow = new Date();
console.log("[DATE NOW]", dateNow);
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("");
console.log("[DATA AWAL]", data?.awalVote);
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 });
// console.log("[DATA]", JSON.stringify(response.data, null, 2));
if (response.success) {
setData(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
}
};
const onLoadCheckContribution = async () => {
try {
const response = await apiVotingCheckContribution({
id: id as string,
authorId: user?.id as string,
});
console.log("[DATA CONTRIBUION]", response.data);
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({
@@ -49,15 +120,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"}>
{dateNow < new Date(data?.awalVote) && (
<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." />
)}
<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 ========= */}

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

@@ -26,7 +26,6 @@ export default function ScreenUpload() {
dirId: DIRECTORY_ID.profile_foto, dirId: DIRECTORY_ID.profile_foto,
}); });
console.log("Response", JSON.stringify(response, null, 2));
if (response.success) { if (response.success) {
await AsyncStorage.setItem("idImage", response.data.id); await AsyncStorage.setItem("idImage", response.data.id);
router.back(); router.back();

View File

@@ -2,7 +2,7 @@
import { apiConfig } from "@/service/api-config"; import { apiConfig } from "@/service/api-config";
import Toast from "react-native-toast-message"; import Toast from "react-native-toast-message";
export async function tryUploadService({ export default async function tryUploadService({
dirId, dirId,
imageUri, imageUri,
}: { }: {

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 395 KiB

After

Width:  |  Height:  |  Size: 30 KiB

BIN
assets/images/loading.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

585
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -9,11 +9,13 @@ export default function ActionIcon({
onPress, onPress,
icon, icon,
size = "md", size = "md",
disabled = false,
}: { }: {
href?: Href; href?: Href;
onPress?: () => void; onPress?: () => void;
icon: React.ReactNode; icon: React.ReactNode;
size?: SizeType; size?: SizeType;
disabled?: boolean;
}) { }) {
const sizeMap = { const sizeMap = {
xs: 22, xs: 22,
@@ -25,7 +27,7 @@ export default function ActionIcon({
const getSize = (size: SizeType): DimensionValue => { const getSize = (size: SizeType): DimensionValue => {
if (!size) return sizeMap.md; // Default to 'md' if size is undefined if (!size) return sizeMap.md; // Default to 'md' if size is undefined
if (typeof size === 'string' && size in sizeMap) { if (typeof size === "string" && size in sizeMap) {
return sizeMap[size as keyof typeof sizeMap]; return sizeMap[size as keyof typeof sizeMap];
} }
return size as DimensionValue; return size as DimensionValue;
@@ -35,9 +37,10 @@ export default function ActionIcon({
return ( return (
<TouchableOpacity <TouchableOpacity
disabled={disabled}
activeOpacity={0.7} activeOpacity={0.7}
style={{ style={{
backgroundColor: MainColor.yellow, backgroundColor: disabled ? MainColor.disabled : MainColor.yellow,
padding: 5, padding: 5,
borderRadius: 50, borderRadius: 50,
alignItems: "center", alignItems: "center",
@@ -46,6 +49,7 @@ export default function ActionIcon({
height: iconSize, height: iconSize,
}} }}
onPress={() => { onPress={() => {
if (disabled) return;
if (href) { if (href) {
router.push(href); router.push(href);
} else { } else {

View File

@@ -40,7 +40,7 @@ export default function BaseBox({
return ( return (
<> <>
{onPress || href ? ( {onPress as any || href ? (
<TouchableOpacity <TouchableOpacity
activeOpacity={0.7} activeOpacity={0.7}
onPress={href ? () => router.navigate(href) : onPress} onPress={href ? () => router.navigate(href) : onPress}

View File

@@ -61,7 +61,7 @@ const ButtonCustom: React.FC<ButtonProps> = ({
activeOpacity={0.8} activeOpacity={0.8}
> >
{/* Render icon jika tersedia */} {/* Render icon jika tersedia */}
{iconLeft && iconLeft} {isLoading ? "" : iconLeft && iconLeft}
{isLoading ? ( {isLoading ? (
<ActivityIndicator size={18} color={MainColor.darkblue} /> <ActivityIndicator size={18} color={MainColor.darkblue} />
) : ( ) : (

View File

@@ -1,4 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
// DateTimeInput.tsx // DateTimeInput.tsx
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { GStyles } from "@/styles/global-styles"; import { GStyles } from "@/styles/global-styles";
@@ -119,7 +119,7 @@ const DateTimeInput_Android: React.FC<DateTimeInputProps> = ({
<Ionicons <Ionicons
name="calendar-outline" name="calendar-outline"
size={20} size={20}
color={MainColor.placeholder} color={disabled ? MainColor.white_gray : MainColor.placeholder}
/> />
</View> </View>
@@ -131,8 +131,8 @@ const DateTimeInput_Android: React.FC<DateTimeInputProps> = ({
}} }}
> >
<Grid.Col span={6} style={{}}> <Grid.Col span={6} style={{}}>
<Pressable onPress={toggleDatePicker}> <Pressable onPress={() => !disabled && toggleDatePicker()}>
<TextCustom color="gray"> <TextCustom color={disabled ? "default" : "gray"}>
{selectedDate ? ( {selectedDate ? (
<TextCustom color="black"> <TextCustom color="black">
{selectedDate.toLocaleDateString()} {selectedDate.toLocaleDateString()}
@@ -148,8 +148,8 @@ const DateTimeInput_Android: React.FC<DateTimeInputProps> = ({
</Grid.Col> </Grid.Col>
<Grid.Col span={5} style={{}}> <Grid.Col span={5} style={{}}>
<Pressable onPress={toggleTimePicker}> <Pressable onPress={() => !disabled && toggleTimePicker()}>
<TextCustom color="gray"> <TextCustom color={disabled ? "default" : "gray"}>
{selectedTime ? ( {selectedTime ? (
<TextCustom color="black"> <TextCustom color="black">
{selectedTime.toLocaleTimeString("id-ID", { {selectedTime.toLocaleTimeString("id-ID", {

View File

@@ -3,24 +3,19 @@ import { MainColor } from "@/constants/color-palet";
import { GStyles } from "@/styles/global-styles"; import { GStyles } from "@/styles/global-styles";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import DateTimePicker, { import DateTimePicker, {
DateTimePickerEvent, DateTimePickerEvent,
} from "@react-native-community/datetimepicker"; } from "@react-native-community/datetimepicker";
import dayjs from "dayjs"; import dayjs from "dayjs";
import React, { useState } from "react"; import React, { useState } from "react";
import { import { StyleProp, Text, View, ViewStyle } from "react-native";
StyleProp,
Text,
View,
ViewStyle
} from "react-native";
import ClickableCustom from "../Clickable/ClickableCustom"; import ClickableCustom from "../Clickable/ClickableCustom";
import TextCustom from "../Text/TextCustom"; import TextCustom from "../Text/TextCustom";
interface DateTimeInputProps { interface DateTimeInputProps {
// Main // Main
value?: DateTimePickerEvent; value?: DateTimePickerEvent | Date | null;
mode?: "date" | "time"; mode?: "date" | "time";
onChange: (selectedDate: DateTimePickerEvent) => void; onChange: (selectedDate: DateTimePickerEvent | Date | null) => void;
maximumDate?: Date; maximumDate?: Date;
minimumDate?: Date; minimumDate?: Date;
// Main // Main
@@ -74,7 +69,7 @@ const DateTimeInput_IOS: React.FC<DateTimeInputProps> = ({
<ClickableCustom <ClickableCustom
activeOpacity={0.8} activeOpacity={0.8}
style={[GStyles.inputContainerArea, containerStyle]} style={[GStyles.inputContainerArea, containerStyle]}
onPress={handlePress} onPress={() => !disabled && handlePress()}
> >
{label && ( {label && (
<Text style={GStyles.inputLabel}> <Text style={GStyles.inputLabel}>
@@ -95,11 +90,11 @@ const DateTimeInput_IOS: React.FC<DateTimeInputProps> = ({
<Ionicons <Ionicons
name="calendar-outline" name="calendar-outline"
size={20} size={20}
color={MainColor.placeholder} color={disabled ? MainColor.white : MainColor.placeholder}
/> />
</View> </View>
<TextCustom color="gray"> <TextCustom color={disabled ? "default" : "gray"}>
{selectedDate ? ( {selectedDate ? (
<TextCustom color="black"> <TextCustom color="black">
{dayjs(selectedDate).format("DD-MM-YYYY HH:mm")} {dayjs(selectedDate).format("DD-MM-YYYY HH:mm")}

View File

@@ -1,19 +1,17 @@
import { DateTimePickerEvent } from "@react-native-community/datetimepicker";
import {
DateTimePickerEvent,
} from "@react-native-community/datetimepicker";
import React from "react"; import React from "react";
import { Platform } from "react-native"; import { Platform } from "react-native";
import DateTimeInput_Android from "./DataTimeAndroid"; import DateTimeInput_Android from "./DataTimeAndroid";
import DateTimeInput_IOS from "./DateTimeIOS"; import DateTimeInput_IOS from "./DateTimeIOS";
type Props = { type Props = {
value?: Date; value?: Date | DateTimePickerEvent | null;
onChange?: (date: Date) => void; onChange?: (date: Date) => void;
label?: string; label?: string;
required?: boolean; required?: boolean;
maximumDate?: Date; maximumDate?: Date;
minimumDate?: Date; minimumDate?: Date;
disabled?: boolean;
}; };
const DateTimePickerCustom: React.FC<Props> = ({ const DateTimePickerCustom: React.FC<Props> = ({
@@ -23,18 +21,21 @@ const DateTimePickerCustom: React.FC<Props> = ({
required, required,
maximumDate, maximumDate,
minimumDate, minimumDate,
disabled = false,
}) => { }) => {
return ( return (
<> <>
{Platform.OS === "ios" ? ( {Platform.OS === "ios" ? (
<DateTimeInput_IOS <DateTimeInput_IOS
label={label} label={label}
onChange={(date: DateTimePickerEvent) => { onChange={(date: DateTimePickerEvent | Date | null) => {
onChange?.(date as any); onChange?.(date as any);
}} }}
required={required} required={required}
maximumDate={maximumDate} maximumDate={maximumDate}
minimumDate={minimumDate} minimumDate={minimumDate}
disabled={disabled}
value={value as DateTimePickerEvent | Date | null}
/> />
) : ( ) : (
<DateTimeInput_Android <DateTimeInput_Android
@@ -45,6 +46,7 @@ const DateTimePickerCustom: React.FC<Props> = ({
required={required} required={required}
maximumDate={maximumDate} maximumDate={maximumDate}
minimumDate={minimumDate} minimumDate={minimumDate}
disabled={disabled}
/> />
)} )}
</> </>

View File

@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { AccentColor, MainColor } from "@/constants/color-palet"; import { AccentColor, MainColor } from "@/constants/color-palet";
import { TEXT_SIZE_SMALL } from "@/constants/constans-value"; import { TEXT_SIZE_SMALL } from "@/constants/constans-value";
import { StyleSheet, Text, TouchableOpacity, View } from "react-native"; import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
@@ -5,19 +6,23 @@ import { IMenuDrawerItem } from "../_Interface/types";
import { Href } from "expo-router"; import { Href } from "expo-router";
type IMenuDrawerItemProps = { type IMenuDrawerItemProps = {
icon: React.ReactNode; icon: React.ReactNode;
label: string; label: string;
value?: string; value?: string;
path?: Href | string; path?: Href | string;
color?: string; color?: string;
} };
interface MenuDrawerDynamicGridProps { interface MenuDrawerDynamicGridProps {
data: IMenuDrawerItemProps[]; data: IMenuDrawerItemProps[];
columns?: number; columns?: number;
onPressItem?: (item: IMenuDrawerItemProps) => void; onPressItem?: (item: IMenuDrawerItemProps) => void;
} }
const MenuDrawerDynamicGrid = ({ data, columns = 4, onPressItem }: MenuDrawerDynamicGridProps) => { const MenuDrawerDynamicGrid = ({
data,
columns = 4,
onPressItem,
}: MenuDrawerDynamicGridProps) => {
const numColumns = columns; const numColumns = columns;
return ( return (

View File

@@ -0,0 +1,65 @@
import API_STRORAGE from "@/constants/base-url-api-strorage";
import DUMMY_IMAGE from "@/constants/dummy-image-value";
import { Href, router } from "expo-router";
import { TouchableOpacity } from "react-native";
import { Avatar } from "react-native-paper";
type Size = "base" | "sm" | "md" | "lg" | "xl";
const sizeMap = {
base: 40,
sm: 60,
md: 80,
lg: 100,
xl: 120,
};
interface AvatarCompProps {
fileId?: string;
fileIdDefault?: string;
size: Size;
onPress?: () => void | any;
href?: Href | undefined | any;
}
export default function AvatarComp({
fileId,
fileIdDefault,
size,
onPress,
href = `/(application)/(image)/preview-image/${fileId}`,
}: AvatarCompProps) {
const dimension = sizeMap[size];
const avatarImage = () => {
return (
<Avatar.Image
size={dimension}
source={
fileId
? { uri: API_STRORAGE.GET({ fileId }) }
: fileIdDefault
? fileIdDefault
: DUMMY_IMAGE.avatar
}
/>
);
};
return (
<>
{onPress || href ? (
<TouchableOpacity
activeOpacity={0.9}
onPress={
href && fileId ? () => router.navigate(href as any) : onPress
}
>
{avatarImage()}
</TouchableOpacity>
) : (
avatarImage()
)}
</>
);
}

View File

@@ -14,8 +14,8 @@ type Size = "base" | "sm" | "md" | "lg" | "xl";
interface AvatarCustomProps { interface AvatarCustomProps {
source?: ImageSourcePropType; source?: ImageSourcePropType;
size?: Size; size?: Size;
onPress?: () => void; onPress?: () => any;
href?: Href | undefined; href?: Href | undefined | any;
} }
const sizeMap = { const sizeMap = {

View File

@@ -2,7 +2,11 @@ import DUMMY_IMAGE from "@/constants/dummy-image-value";
import { Image } from "react-native"; import { Image } from "react-native";
import BaseBox from "../Box/BaseBox"; import BaseBox from "../Box/BaseBox";
export default function LandscapeFrameUploaded() { export default function LandscapeFrameUploaded({
image,
}: {
image?: string;
}) {
return ( return (
<BaseBox <BaseBox
style={{ style={{
@@ -11,7 +15,7 @@ export default function LandscapeFrameUploaded() {
}} }}
> >
<Image <Image
source={DUMMY_IMAGE.background} source={image ? { uri: image } : DUMMY_IMAGE.dummy_image}
resizeMode="cover" resizeMode="cover"
style={{ width: "100%", height: "100%", borderRadius: 10 }} style={{ width: "100%", height: "100%", borderRadius: 10 }}
/> />

View File

@@ -0,0 +1,8 @@
import { MainColor } from "@/constants/color-palet";
import { ActivityIndicator } from "react-native";
export default function LoaderCustom({ size }: { size?: "small" | "large" }) {
return (
<ActivityIndicator size={size ? size : "small"} color={MainColor.yellow} />
);
}

View File

@@ -6,8 +6,8 @@ export default function IconEdit({
size, size,
color, color,
}: { }: {
size: number; size?: number;
color: string; color?: string;
}) { }) {
return ( return (
<> <>

View File

@@ -1,7 +1,6 @@
import { ImageSourcePropType } from "react-native";
import Divider from "../Divider/Divider"; import Divider from "../Divider/Divider";
import Grid from "../Grid/GridCustom"; import Grid from "../Grid/GridCustom";
import AvatarCustom from "../Image/AvatarCustom"; import AvatarComp from "../Image/AvatarComp";
import TextCustom from "../Text/TextCustom"; import TextCustom from "../Text/TextCustom";
const AvatarUsernameAndOtherComponent = ({ const AvatarUsernameAndOtherComponent = ({
@@ -12,7 +11,7 @@ const AvatarUsernameAndOtherComponent = ({
withBottomLine = false, withBottomLine = false,
}: { }: {
avatarHref?: string; avatarHref?: string;
avatar?: ImageSourcePropType; avatar?: string;
name?: string; name?: string;
rightComponent?: React.ReactNode; rightComponent?: React.ReactNode;
withBottomLine?: boolean; withBottomLine?: boolean;
@@ -21,7 +20,7 @@ const AvatarUsernameAndOtherComponent = ({
<> <>
<Grid containerStyle={{ zIndex: 10 }}> <Grid containerStyle={{ zIndex: 10 }}>
<Grid.Col span={2}> <Grid.Col span={2}>
<AvatarCustom source={avatar} href={avatarHref as any} /> <AvatarComp fileId={avatar} href={avatarHref as any} size="base" />
</Grid.Col> </Grid.Col>
<Grid.Col <Grid.Col
span={rightComponent ? 6 : 10} span={rightComponent ? 6 : 10}

View File

@@ -4,17 +4,33 @@ import { Image } from "expo-image";
import { StyleSheet } from "react-native"; import { StyleSheet } from "react-native";
import ClickableCustom from "../Clickable/ClickableCustom"; import ClickableCustom from "../Clickable/ClickableCustom";
import { router } from "expo-router"; import { router } from "expo-router";
import API_STRORAGE from "@/constants/base-url-api-strorage";
export default function DummyLandscapeImage({height, unClickPath}: {height?: number, unClickPath?: boolean}) { export default function DummyLandscapeImage({
height,
unClickPath,
imageId,
}: {
height?: number;
unClickPath?: boolean;
imageId?: string;
}) {
return ( return (
<ClickableCustom <ClickableCustom
onPress={() => { onPress={() => {
if (!unClickPath) { if (!unClickPath) {
router.push("/(application)/(image)/preview-image/1"); router.push(`/(application)/(image)/preview-image/${imageId}`);
} }
}} }}
> >
<Image source={DUMMY_IMAGE.background} style={[styles.backgroundImage, {height: height || 200}]} /> <Image
source={
imageId
? { uri: API_STRORAGE.GET({ fileId: imageId }) }
: DUMMY_IMAGE.dummy_image
}
style={[styles.backgroundImage, { height: height || 200 }]}
/>
</ClickableCustom> </ClickableCustom>
); );
} }

View File

@@ -11,6 +11,7 @@ interface SearchInputProps {
iconRight?: React.ReactNode; iconRight?: React.ReactNode;
containerStyle?: StyleProp<ViewStyle>; containerStyle?: StyleProp<ViewStyle>;
style?: StyleProp<TextStyle>; style?: StyleProp<TextStyle>;
onChangeText?: (value: string) => void;
} }
export default function SearchInput({ export default function SearchInput({
placeholder, placeholder,
@@ -19,6 +20,7 @@ export default function SearchInput({
iconRight, iconRight,
containerStyle, containerStyle,
style, style,
onChangeText,
...props ...props
}: SearchInputProps) { }: SearchInputProps) {
return ( return (
@@ -30,6 +32,7 @@ export default function SearchInput({
color={MainColor.placeholder} color={MainColor.placeholder}
/> />
} }
onChangeText={onChangeText}
placeholder={placeholder} placeholder={placeholder}
borderRadius={50} borderRadius={50}
containerStyle={[containerStyle, { marginBottom: 0 }]} containerStyle={[containerStyle, { marginBottom: 0 }]}

View File

@@ -38,6 +38,7 @@ import StackCustom from "./Stack/StackCustom";
import SelectCustom from "./Select/SelectCustom"; import SelectCustom from "./Select/SelectCustom";
// Image // Image
import AvatarCustom from "./Image/AvatarCustom"; import AvatarCustom from "./Image/AvatarCustom";
import AvatarComp from "./Image/AvatarComp";
import LandscapeFrameUploaded from "./Image/LandscapeFrameUploaded"; import LandscapeFrameUploaded from "./Image/LandscapeFrameUploaded";
// Divider // Divider
import Divider from "./Divider/Divider"; import Divider from "./Divider/Divider";
@@ -60,6 +61,8 @@ import DummyLandscapeImage from "./_ShareComponent/DummyLandscapeImage";
import GridComponentView from "./_ShareComponent/GridSectionView"; import GridComponentView from "./_ShareComponent/GridSectionView";
// Progress // Progress
import ProgressCustom from "./Progress/ProgressCustom"; import ProgressCustom from "./Progress/ProgressCustom";
// Loader
import LoaderCustom from "./Loader/LoaderCustom";
export { export {
// ActionIcon // ActionIcon
@@ -69,6 +72,7 @@ export {
AlertDefaultSystem, AlertDefaultSystem,
// Image // Image
AvatarCustom, AvatarCustom,
AvatarComp,
// ShareComponent // ShareComponent
AvatarUsernameAndOtherComponent, AvatarUsernameAndOtherComponent,
// Button // Button
@@ -126,4 +130,6 @@ export {
TextInputCustom, TextInputCustom,
// ViewWrapper // ViewWrapper
ViewWrapper, ViewWrapper,
// Loader
LoaderCustom,
}; };

View File

@@ -1,6 +1,7 @@
const DUMMY_IMAGE = { const DUMMY_IMAGE = {
avatar: require("@/assets/images/dummy/dummy-user.png"), avatar: require("@/assets/images/dummy/dummy-user.png"),
background: require("@/assets/images/dummy/dummy-image-background.jpg"), background: require("@/assets/images/dummy/dummy-image-background.jpg"),
dummy_image: require("@/assets/images/dummy/dummy-image.jpg"),
}; };
export default DUMMY_IMAGE; export default DUMMY_IMAGE;

View File

@@ -92,7 +92,7 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => {
await AsyncStorage.setItem("authToken", token); await AsyncStorage.setItem("authToken", token);
const responseUser = await apiConfig.get( const responseUser = await apiConfig.get(
`/mobile/user?token=${token}`, `/mobile?token=${token}`,
{ {
headers: { headers: {
Authorization: `Bearer ${token}`, Authorization: `Bearer ${token}`,

18107
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,65 +11,69 @@
"lint": "expo lint" "lint": "expo lint"
}, },
"dependencies": { "dependencies": {
"@expo/vector-icons": "^14.1.0", "@expo/vector-icons": "^15.0.2",
"@react-native-async-storage/async-storage": "2.1.2", "@react-native-async-storage/async-storage": "2.2.0",
"@react-native-community/datetimepicker": "8.4.1", "@react-native-community/datetimepicker": "8.4.4",
"@react-navigation/bottom-tabs": "^7.4.2", "@react-navigation/bottom-tabs": "^7.3.10",
"@react-navigation/drawer": "^7.5.2", "@react-navigation/drawer": "^7.3.9",
"@react-navigation/elements": "^2.3.8", "@react-navigation/elements": "^2.3.8",
"@react-navigation/native": "^7.1.6", "@react-navigation/native": "^7.1.6",
"@react-navigation/native-stack": "^7.3.21", "@react-navigation/native-stack": "^7.3.10",
"@types/lodash": "^4.17.20", "@types/lodash": "^4.17.20",
"@types/react-native-vector-icons": "^6.4.18", "@types/react-native-vector-icons": "^6.4.18",
"axios": "^1.11.0", "axios": "^1.11.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"expo": "53.0.17", "expo": "^54.0.0",
"expo-blur": "~14.1.5", "expo-blur": "~15.0.7",
"expo-camera": "~16.1.10", "expo-camera": "~17.0.7",
"expo-clipboard": "~7.1.5", "expo-clipboard": "~8.0.7",
"expo-constants": "~17.1.7", "expo-constants": "~18.0.8",
"expo-dev-client": "~5.2.4", "expo-dev-client": "~6.0.12",
"expo-document-picker": "~13.1.6", "expo-document-picker": "~14.0.7",
"expo-file-system": "~18.1.11", "expo-file-system": "~19.0.12",
"expo-font": "~13.3.2", "expo-font": "~14.0.8",
"expo-haptics": "~14.1.4", "expo-haptics": "~15.0.7",
"expo-image": "~2.3.2", "expo-image": "~3.0.8",
"expo-image-picker": "~16.1.4", "expo-image-picker": "~17.0.8",
"expo-linking": "~7.1.7", "expo-linking": "~8.0.8",
"expo-router": "~5.1.3", "expo-router": "~6.0.1",
"expo-splash-screen": "~0.30.10", "expo-splash-screen": "~31.0.9",
"expo-status-bar": "~2.2.3", "expo-status-bar": "~3.0.8",
"expo-symbols": "~0.4.5", "expo-symbols": "~1.0.7",
"expo-system-ui": "~5.0.10", "expo-system-ui": "~6.0.7",
"expo-web-browser": "~14.2.0", "expo-web-browser": "~15.0.7",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"react": "19.0.0", "react": "19.1.0",
"react-dom": "19.0.0", "react-dom": "19.1.0",
"react-native": "0.79.5", "react-native": "0.81.4",
"react-native-dotenv": "^3.4.11", "react-native-dotenv": "^3.4.11",
"react-native-gesture-handler": "~2.24.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-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.7.1", "react-native-pager-view": "6.9.1",
"react-native-paper": "^5.14.5", "react-native-paper": "^5.14.5",
"react-native-qrcode-svg": "^6.3.15", "react-native-qrcode-svg": "^6.3.15",
"react-native-reanimated": "~3.17.4", "react-native-reanimated": "~4.1.0",
"react-native-safe-area-context": "5.4.0", "react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.11.1", "react-native-screens": "~4.16.0",
"react-native-svg": "15.11.2", "react-native-svg": "15.12.1",
"react-native-toast-message": "^2.3.3", "react-native-toast-message": "^2.3.3",
"react-native-vector-icons": "^10.2.0", "react-native-vector-icons": "^10.2.0",
"react-native-web": "~0.20.0", "react-native-web": "^0.21.0",
"react-native-webview": "13.13.5" "react-native-webview": "13.15.0",
"react-native-worklets": "^0.5.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "^7.25.2", "@babel/core": "^7.25.2",
"@types/react": "~19.0.10", "@types/react": "~19.1.10",
"eslint": "^9.25.0", "eslint": "^9.25.0",
"eslint-config-expo": "~9.2.0", "eslint-config-expo": "~10.0.0",
"expo-module-scripts": "^4.1.10", "expo-module-scripts": "^4.1.10",
"typescript": "~5.8.3" "typescript": "~5.9.2"
}, },
"private": true "private": true,
"engines": {
"bun": ">=1.0.0"
}
} }

View File

@@ -14,7 +14,6 @@ import Toast from "react-native-toast-message";
export default function RegisterView() { export default function RegisterView() {
const { nomor } = useLocalSearchParams(); const { nomor } = useLocalSearchParams();
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
// const [loading, setLoading] = useState(false);
const { registerUser, isLoading } = useAuth(); const { registerUser, isLoading } = useAuth();
@@ -22,16 +21,22 @@ export default function RegisterView() {
if (!nomor) { if (!nomor) {
Toast.show({ Toast.show({
type: "error", type: "error",
text1: "Gagal", text1: "Lengkapi nomor",
text2: "Nomor tidak ditemukan",
}); });
return false; return false;
} }
if (!username) { if (!username) {
Toast.show({ Toast.show({
type: "error", type: "error",
text1: "Gagal", text1: "Username tidak boleh kosong",
text2: "Username tidak boleh kosong", });
return false;
}
if (username.includes(" ")) {
Toast.show({
type: "info",
text1: "Username tidak boleh mengandung spasi",
}); });
return false; return false;
} }
@@ -42,9 +47,20 @@ export default function RegisterView() {
const isValid = validasiData(); const isValid = validasiData();
if (!isValid) return; if (!isValid) return;
if (username.length < 5) {
Toast.show({
type: "info",
text1: "Info",
text2: "Username minimal 5 karakter",
});
return;
}
const usernameLower = username.toLowerCase();
await registerUser({ await registerUser({
nomor: nomor as string, nomor: nomor as string,
username: username, username: usernameLower,
}); });
} }
@@ -73,6 +89,11 @@ export default function RegisterView() {
placeholder="Masukkan username" placeholder="Masukkan username"
value={username} value={username}
onChangeText={(text) => setUsername(text)} onChangeText={(text) => setUsername(text)}
error={
username.includes(" ")
? "Username tidak boleh mengandung spasi"
: ""
}
/> />
<ButtonCustom isLoading={isLoading} onPress={handleRegister}> <ButtonCustom isLoading={isLoading} onPress={handleRegister}>

View File

@@ -1,21 +1,49 @@
import { import { AvatarUsernameAndOtherComponent, BoxWithHeaderSection, Grid, StackCustom, TextCustom } from "@/components";
BaseBox, import { dateTimeView } from "@/utils/dateTimeView";
Grid,
StackCustom,
TextCustom
} from "@/components";
export default function Event_BoxDetailPublishSection({ export default function Event_BoxDetailPublishSection({
data,
footerButton, footerButton,
}: { }: {
data: any;
footerButton?: React.ReactNode; footerButton?: React.ReactNode;
}) { }) {
const listData = [
{
title: "Lokasi",
value: data?.lokasi || "-",
},
{
title: "Tipe Acara",
value: data?.EventMaster_TipeAcara?.name || "-",
},
{
title: "Tanggal Mulai",
value: dateTimeView({ date: data?.tanggal }) || "-",
},
{
title: "Tanggal Berakhir",
value: dateTimeView({ date: data?.tanggalSelesai }) || "-",
},
{
title: "Deskripsi",
value: data?.deskripsi || "-",
},
];
return ( return (
<> <>
<BaseBox> <BoxWithHeaderSection>
<StackCustom> <StackCustom>
<AvatarUsernameAndOtherComponent
avatarHref={`/profile/${data?.Author?.Profile?.id}`}
name={data?.Author?.username || "-"}
avatar={data?.Author?.Profile?.imageId || ""}
/>
<TextCustom bold align="center" size="xlarge"> <TextCustom bold align="center" size="xlarge">
Judul event publish {data?.title || "-"}
</TextCustom> </TextCustom>
{listData.map((item, index) => ( {listData.map((item, index) => (
<Grid key={index}> <Grid key={index}>
@@ -28,34 +56,9 @@ export default function Event_BoxDetailPublishSection({
</Grid> </Grid>
))} ))}
</StackCustom> </StackCustom>
</BaseBox> </BoxWithHeaderSection>
{footerButton} {footerButton}
</> </>
); );
} }
const listData = [
{
title: "Lokasi",
value:
"Lorem ipsum dolor sit amet consectetur adipisicing elit. Consectetur eveniet ab eum ducimus tempore a quia deserunt quisquam. Tempora, atque. Aperiam minima asperiores dicta perferendis quis adipisci, dolore optio porro!",
},
{
title: "Tipe Acara",
value: "Workshop",
},
{
title: "Tanggal Mulai",
value: "Senin, 18 Juli 2025, 10:00 WIB",
},
{
title: "Tanggal Berakhir",
value: "Selasa, 19 Juli 2025, 12:00 WIB",
},
{
title: "Deskripsi",
value:
"Lorem ipsum dolor sit amet consectetur adipisicing elit. Consectetur eveniet ab eum ducimus tempore a quia deserunt quisquam. Tempora, atque. Aperiam minima asperiores dicta perferendis quis adipisci, dolore optio porro!",
},
];

View File

@@ -7,21 +7,15 @@ import {
import { Href } from "expo-router"; import { Href } from "expo-router";
export default function Event_BoxPublishSection({ export default function Event_BoxPublishSection({
id,
title,
username,
description,
href, href,
data,
// Avatar // Avatar
sourceAvatar, sourceAvatar,
rightComponentAvatar, rightComponentAvatar,
}: { }: {
id: string;
title?: string;
username?: string;
description?: string;
href: Href; href: Href;
data: any;
// Avatar // Avatar
sourceAvatar?: string; sourceAvatar?: string;
@@ -32,18 +26,15 @@ export default function Event_BoxPublishSection({
<BoxWithHeaderSection href={href}> <BoxWithHeaderSection href={href}>
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
<AvatarUsernameAndOtherComponent <AvatarUsernameAndOtherComponent
avatarHref={`/profile/${id}`} avatarHref={`/profile/${data?.Author?.Profile?.id}`}
name={username || "Lorem ipsum dolor sit"} name={data?.Author?.username || "-"}
rightComponent={rightComponentAvatar} rightComponent={rightComponentAvatar}
avatar={sourceAvatar as any} avatar={data?.Author?.Profile?.imageId || ""}
/> />
<TextCustom truncate bold> <TextCustom truncate bold>
{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?.deskripsi || "-"}</TextCustom>
</StackCustom> </StackCustom>
</BoxWithHeaderSection> </BoxWithHeaderSection>
</> </>

View File

@@ -1,28 +1,158 @@
import { ButtonCustom, Grid } from "@/components"; import { AlertDefaultSystem, ButtonCustom, Grid } from "@/components";
import { View } from "react-native"; import {
apiEventDelete,
apiEventUpdateStatus,
} from "@/service/api-client/api-event";
import { router } from "expo-router";
import Toast from "react-native-toast-message";
export default function Event_ButtonStatusSection({ export default function Event_ButtonStatusSection({
id,
status, status,
onOpenAlert,
onOpenDeleteAlert,
}: { }: {
id: string;
status: string; status: string;
onOpenAlert: (value: boolean) => void;
onOpenDeleteAlert: (value: boolean) => void;
}) { }) {
const handleBatalkanReview = () => {
AlertDefaultSystem({
title: "Batalkan Review",
message: "Apakah Anda yakin ingin batalkan review ini?",
textLeft: "Batal",
textRight: "Ya",
onPressRight: async () => {
try {
const response = await apiEventUpdateStatus({
id: id,
status: "draft",
});
const handleOpenAlert = () => { if (response.success) {
onOpenAlert(true); 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);
}
},
});
};
const handleAjukanReview = () => {
AlertDefaultSystem({
title: "Ajukan Review",
message: "Apakah Anda yakin ingin ajukan review ini?",
textLeft: "Batal",
textRight: "Ya",
onPressRight: async () => {
try {
const response = await apiEventUpdateStatus({
id: id,
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);
}
},
});
};
const handleEditKembali = () => {
AlertDefaultSystem({
title: "Edit Kembali",
message: "Apakah Anda yakin ingin edit kembali ini?",
textLeft: "Batal",
textRight: "Ya",
onPressRight: async () => {
try {
const response = await apiEventUpdateStatus({
id: id,
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);
}
},
});
}; };
const handleOpenDeleteAlert = () => { const handleOpenDeleteAlert = () => {
onOpenDeleteAlert(true); AlertDefaultSystem({
title: "Hapus",
message: "Apakah Anda yakin ingin menghapus data ini?",
textLeft: "Batal",
textRight: "Hapus",
onPressRight: async () => {
try {
const response = await apiEventDelete({ id: id });
if (response.success) {
Toast.show({
type: "success",
text1: "Data dihapus",
});
router.back();
} else {
Toast.show({
type: "info",
text1: "Info",
text2: response.message,
});
router.back();
}
} catch (error) {
console.log("[ERROR]", error);
}
},
});
}; };
const DeleteButton = () => { const DeleteButton = () => {
return ( return (
<> <>
<ButtonCustom backgroundColor="red" textColor="white" onPress={handleOpenDeleteAlert}> <ButtonCustom
backgroundColor="red"
textColor="white"
onPress={handleOpenDeleteAlert}
>
Hapus Hapus
</ButtonCustom> </ButtonCustom>
</> </>
@@ -35,7 +165,7 @@ export default function Event_ButtonStatusSection({
case "review": case "review":
return ( return (
<ButtonCustom onPress={handleOpenAlert}> <ButtonCustom onPress={handleBatalkanReview}>
Batalkan Review Batalkan Review
</ButtonCustom> </ButtonCustom>
); );
@@ -44,13 +174,14 @@ export default function Event_ButtonStatusSection({
return ( return (
<> <>
<Grid> <Grid>
<Grid.Col span={5}> <Grid.Col span={6} style={{ paddingRight: 10 }}>
<ButtonCustom onPress={handleOpenAlert}>Ajukan Review</ButtonCustom> <ButtonCustom onPress={handleAjukanReview}>
Ajukan Review
</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>
</> </>
); );
@@ -59,13 +190,14 @@ export default function Event_ButtonStatusSection({
return ( return (
<> <>
<Grid> <Grid>
<Grid.Col span={5}> <Grid.Col span={6} style={{ paddingRight: 10 }}>
<ButtonCustom onPress={handleOpenAlert}>Edit Kembali</ButtonCustom> <ButtonCustom onPress={handleEditKembali}>
Edit Kembali
</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,28 +1,34 @@
import { BaseBox, StackCustom, DummyLandscapeImage, TextCustom } from "@/components"; import {
BaseBox,
StackCustom,
DummyLandscapeImage,
TextCustom,
} from "@/components";
export default function Job_BoxDetailSection({data}: {data: any}) { export default function Job_BoxDetailSection({ data }: { data: any }) {
return ( return (
<> <>
<BaseBox> <BaseBox>
<StackCustom gap={"lg"}> <StackCustom gap={"lg"}>
<DummyLandscapeImage /> {data && data.imageId && (
<DummyLandscapeImage imageId={data?.imageId} />
)}
<TextCustom align="center" bold size="large"> <TextCustom align="center" bold size="large">
{data?.posisi} {data?.title || "-"}
</TextCustom> </TextCustom>
<StackCustom gap={"sm"}> <StackCustom gap={"sm"}>
<TextCustom bold>Syarat & Ketentuan :</TextCustom> <TextCustom bold>Syarat & Ketentuan :</TextCustom>
<TextCustom>{data?.syaratKetentuan}</TextCustom> <TextCustom>{data?.content || "-"}</TextCustom>
</StackCustom>
<StackCustom gap={"sm"}>
<TextCustom bold>Deskripsi :</TextCustom>
<TextCustom>{data?.deskripsi}</TextCustom>
</StackCustom>
</StackCustom> </StackCustom>
</BaseBox>
</> <StackCustom gap={"sm"}>
); <TextCustom bold>Deskripsi :</TextCustom>
<TextCustom>{data?.deskripsi || "-"}</TextCustom>
</StackCustom>
</StackCustom>
</BaseBox>
</>
);
} }

View File

@@ -1,11 +1,24 @@
import { AlertDefaultSystem, ButtonCustom, Grid } from "@/components"; import { AlertDefaultSystem, ButtonCustom, Grid } from "@/components";
import {
apiJobDelete,
apiJobUpdateData,
apiJobUpdateStatus,
} from "@/service/api-client/api-job";
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 Job_ButtonStatusSection({ export default function Job_ButtonStatusSection({
id,
status, status,
isLoading,
onSetLoading,
isArchive,
}: { }: {
id: string;
status: string; status: string;
isLoading: boolean;
onSetLoading: (value: boolean) => void;
isArchive?: boolean;
}) { }) {
const handleBatalkanReview = () => { const handleBatalkanReview = () => {
AlertDefaultSystem({ AlertDefaultSystem({
@@ -13,9 +26,33 @@ export default function Job_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 apiJobUpdateStatus({
id: id,
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 +63,33 @@ export default function Job_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 apiJobUpdateStatus({
id: id,
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 +100,33 @@ export default function Job_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 apiJobUpdateStatus({
id: id,
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 +137,71 @@ export default function Job_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 apiJobDelete({
id: id,
});
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);
}
},
});
};
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);
}
}, },
}); });
}; };
@@ -63,6 +210,7 @@ export default function Job_ButtonStatusSection({
return ( return (
<> <>
<ButtonCustom <ButtonCustom
isLoading={isLoading}
backgroundColor="red" backgroundColor="red"
textColor="white" textColor="white"
onPress={handleOpenDeleteAlert} onPress={handleOpenDeleteAlert}
@@ -78,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
@@ -90,7 +238,7 @@ export default function Job_ButtonStatusSection({
case "review": case "review":
return ( return (
<ButtonCustom onPress={handleBatalkanReview}> <ButtonCustom isLoading={isLoading} onPress={handleBatalkanReview}>
Batalkan Review Batalkan Review
</ButtonCustom> </ButtonCustom>
); );
@@ -99,15 +247,14 @@ export default function Job_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={{ paddingLeft: 10 }}>
<View /> {DeleteButton()}
</Grid.Col> </Grid.Col>
<Grid.Col span={5}>{DeleteButton()}</Grid.Col>
</Grid> </Grid>
</> </>
); );
@@ -116,15 +263,15 @@ export default function Job_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}>
<View /> <Grid.Col span={6} style={{ paddingRight: 10 }}>
{DeleteButton()}
</Grid.Col> </Grid.Col>
<Grid.Col span={5}>{DeleteButton()}</Grid.Col>
</Grid> </Grid>
</> </>
); );

View File

@@ -0,0 +1,42 @@
import { BaseBox, Grid, TextCustom } from "@/components";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { Ionicons } from "@expo/vector-icons";
import { router } from "expo-router";
export default function Portofolio_BoxView({ data }: { data: any }) {
return (
<>
<BaseBox
style={{ backgroundColor: MainColor.darkblue }}
onPress={() => {
router.push(`/portofolio/${data?.id}`);
}}
>
<Grid>
<Grid.Col
span={10}
style={{ justifyContent: "center", backgroundColor: "" }}
>
<TextCustom bold size="large" truncate={1}>
{data && data?.namaBisnis ? data.namaBisnis : "-"}
</TextCustom>
<TextCustom size="small" color="yellow">
#{data && data?.id_Portofolio ? data.id_Portofolio : "-"}
</TextCustom>
</Grid.Col>
<Grid.Col
span={2}
style={{ alignItems: "flex-end", justifyContent: "center" }}
>
<Ionicons
name="caret-forward"
size={ICON_SIZE_SMALL}
color="white"
/>
</Grid.Col>
</Grid>
</BaseBox>
</>
);
}

View File

@@ -0,0 +1,132 @@
import { BoxButtonOnFooter, ButtonCustom } from "@/components";
import DIRECTORY_ID from "@/constants/directory-id";
import { apiPortofolioCreate } from "@/service/api-client/api-portofolio";
import { uploadImageService } from "@/service/upload-service";
import { router } from "expo-router";
import Toast from "react-native-toast-message";
interface Props {
id: string;
data: any;
dataMedsos: any;
imageUri: string | null;
subBidangSelected: any[];
isLoadingCreate: boolean;
setIsLoadingCreate: (value: boolean) => void;
}
interface ICreatePortofolio {
id: string;
namaBisnis: string;
masterBidangBisnisId: string;
alamatKantor: string;
tlpn: string;
deskripsi: string;
fileId: string;
facebook: string;
twitter: string;
instagram: string;
tiktok: string;
youtube: string;
subBidang: any[];
}
export default function Portofolio_ButtonCreate({
id,
data,
dataMedsos,
imageUri,
subBidangSelected,
isLoadingCreate,
setIsLoadingCreate,
}: Props) {
const validaasiData = () => {
if (
!data.namaBisnis ||
!data.masterBidangBisnisId ||
!data.alamatKantor ||
!data.tlpn ||
!data.deskripsi ||
subBidangSelected.map((item) => item.id).includes("") ||
subBidangSelected.map((item) => item.id).includes(null)
) {
return false;
}
return true;
};
const handleCreatePortofolio = async () => {
console.log(
"Data sub bidang >>",
JSON.stringify(subBidangSelected, null, 2)
);
if (!validaasiData()) {
Toast.show({
type: "info",
text1: "Info",
text2: "Harap lengkapi data",
});
return;
}
try {
setIsLoadingCreate(true);
let fileId = "";
if (imageUri) {
const response = await uploadImageService({
imageUri: imageUri,
dirId: DIRECTORY_ID.portofolio_logo,
});
fileId = response.data.id;
}
// Fix Data
const newData: ICreatePortofolio = {
id: id,
namaBisnis: data.namaBisnis,
masterBidangBisnisId: data.masterBidangBisnisId,
alamatKantor: data.alamatKantor,
tlpn: data.tlpn,
deskripsi: data.deskripsi,
fileId: fileId,
facebook: dataMedsos.facebook,
twitter: dataMedsos.twitter,
instagram: dataMedsos.instagram,
tiktok: dataMedsos.tiktok,
youtube: dataMedsos.youtube,
subBidang: subBidangSelected,
};
const response = await apiPortofolioCreate({
data: newData,
});
console.log("Response >>", JSON.stringify(response, null, 2));
// return router.replace(`/maps/create`);
// return router.push(`/maps/create`);
Toast.show({
type: "success",
text1: "Sukses",
text2: "Data berhasil disimpan",
});
return router.back();
} catch (error) {
Toast.show({
type: "error",
text1: "Error",
text2: error as string,
});
} finally {
setIsLoadingCreate(false);
}
};
return (
<BoxButtonOnFooter>
<ButtonCustom
isLoading={isLoadingCreate}
onPress={handleCreatePortofolio}
>
Selanjutnya
</ButtonCustom>
</BoxButtonOnFooter>
);
}

View File

@@ -1,17 +1,53 @@
import { ButtonCustom } from "@/components"; import { AlertDefaultSystem, ButtonCustom } from "@/components";
import { MainColor } from "@/constants/color-palet"; import { MainColor } from "@/constants/color-palet";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
import { apiDeletePortofolio } from "@/service/api-client/api-portofolio";
import { router } from "expo-router";
export default function Portofolio_ButtonDelete({ export default function Portofolio_ButtonDelete({
setShowDeleteAlert, id,
isLoadingDelete,
setIsLoadingDelete,
}: { }: {
setShowDeleteAlert: (value: boolean) => void; id: string;
isLoadingDelete: boolean;
setIsLoadingDelete: (value: boolean) => void;
}) { }) {
const handleDelete = () => { const handleDelete = async () => {
setShowDeleteAlert(true); AlertDefaultSystem({
title: "Hapus Portofolio",
message: "Yakin ingin menghapus portofolio ini?",
textLeft: "Batal",
textRight: "Hapus",
onPressRight: async () => {
try {
setIsLoadingDelete(true);
const response = await apiDeletePortofolio({ id: id });
console.log("Response portofolio >>", response);
if (response.success) {
console.log("Portofolio berhasil dihapus");
router.back();
}
console.log("Gagal dihapus >>", response);
} catch (error) {
console.log("Error delete portofolio >>", error);
} finally {
setIsLoadingDelete(false);
}
},
})
}; };
return ( return (
<ButtonCustom textColor={MainColor.white} iconLeft={<Ionicons name="trash-outline" size={20} color="white" />} onPress={handleDelete} backgroundColor={MainColor.red}> <ButtonCustom
isLoading={isLoadingDelete}
textColor={MainColor.white}
iconLeft={<Ionicons name="trash-outline" size={20} color="white" />}
onPress={handleDelete}
backgroundColor={MainColor.red}
>
Hapus Hapus
</ButtonCustom> </ButtonCustom>
); );

View File

@@ -1,4 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { import {
AvatarComp,
AvatarCustom, AvatarCustom,
BaseBox, BaseBox,
CenterCustom, CenterCustom,
@@ -10,11 +12,18 @@ import {
import DividerCustom from "@/components/Divider/DividerCustom"; import DividerCustom from "@/components/Divider/DividerCustom";
import { AccentColor } from "@/constants/color-palet"; import { AccentColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value"; import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import DUMMY_IMAGE from "@/constants/dummy-image-value";
import { FontAwesome, Ionicons } from "@expo/vector-icons"; import { FontAwesome, Ionicons } from "@expo/vector-icons";
import { router, useLocalSearchParams } from "expo-router"; import { router, useLocalSearchParams } from "expo-router";
import { View } from "react-native"; import { View } from "react-native";
export default function Portofolio_Data() { export default function Portofolio_Data({
data,
listSubBidang,
}: {
data: any;
listSubBidang: any[];
}) {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const listData = [ const listData = [
@@ -22,60 +31,65 @@ export default function Portofolio_Data() {
icon: ( icon: (
<FontAwesome name="building-o" size={ICON_SIZE_SMALL} color="white" /> <FontAwesome name="building-o" size={ICON_SIZE_SMALL} color="white" />
), ),
label: "PT.Bali Interakrtif Perkasa", label: data && data?.namaBisnis ? data.namaBisnis : "-",
}, },
{ {
icon: ( icon: (
<Ionicons name="call-outline" size={ICON_SIZE_SMALL} color="white" /> <Ionicons name="call-outline" size={ICON_SIZE_SMALL} color="white" />
), ),
label: "+6282340374412", label: data && data?.tlpn ? data.tlpn : "-",
}, },
{ {
icon: ( icon: (
<Ionicons name="home-outline" size={ICON_SIZE_SMALL} color="white" /> <Ionicons name="home-outline" size={ICON_SIZE_SMALL} color="white" />
), ),
label: "Jl. Raya Kuta No. 123, Bandung, Indonesia", label: data && data?.alamatKantor ? data.alamatKantor : "-",
}, },
{ {
icon: ( icon: (
<Ionicons name="list-outline" size={ICON_SIZE_SMALL} color="white" /> <Ionicons name="list-outline" size={ICON_SIZE_SMALL} color="white" />
), ),
label: "Teknologia", label:
data && data?.MasterBidangBisnis?.name
? data.MasterBidangBisnis.name
: "-",
}, },
]; ];
const listSubBidang = [ // const listSubBidang = [
{ // {
icon: ( // icon: (
<Ionicons // <Ionicons
name="chevron-forward-outline" // name="chevron-forward-outline"
size={ICON_SIZE_SMALL} // size={ICON_SIZE_SMALL}
color="white" // color="white"
/> // />
), // ),
label: "Security System", // label: "Security System",
}, // },
{ // {
icon: ( // icon: (
<Ionicons // <Ionicons
name="chevron-forward-outline" // name="chevron-forward-outline"
size={ICON_SIZE_SMALL} // size={ICON_SIZE_SMALL}
color="white" // color="white"
/> // />
), // ),
label: "Web Developers", // label: "Web Developers",
}, // },
{ // {
icon: ( // icon: (
<Ionicons // <Ionicons
name="chevron-forward-outline" // name="chevron-forward-outline"
size={ICON_SIZE_SMALL} // size={ICON_SIZE_SMALL}
color="white" // color="white"
/> // />
), // ),
label: "Mobile Developers", // label: "Mobile Developers",
}, // },
]; // ];
// console.log("List Sub Bidang >>", JSON.stringify(listSubBidang, null, 2));
return ( return (
<> <>
@@ -86,7 +100,9 @@ export default function Portofolio_Data() {
<TextCustom bold>Data Bisnis</TextCustom> <TextCustom bold>Data Bisnis</TextCustom>
</Grid.Col> </Grid.Col>
<Grid.Col span={6} style={{ alignItems: "flex-end" }}> <Grid.Col span={6} style={{ alignItems: "flex-end" }}>
<TextCustom color="yellow">ID: {id}</TextCustom> <TextCustom color="yellow" size={12} bold>
ID: {(data && data?.id_Portofolio) || "-"}
</TextCustom>
</Grid.Col> </Grid.Col>
</Grid> </Grid>
@@ -106,7 +122,11 @@ export default function Portofolio_Data() {
}} }}
> >
<CenterCustom> <CenterCustom>
<AvatarCustom size="xl" /> <AvatarComp
size="xl"
fileId={data?.logoId as any}
fileIdDefault={DUMMY_IMAGE.dummy_image}
/>
</CenterCustom> </CenterCustom>
</ClickableCustom> </ClickableCustom>
@@ -126,14 +146,18 @@ export default function Portofolio_Data() {
</Grid> </Grid>
))} ))}
<View style={{ paddingLeft: 10 }}> <View style={{ paddingLeft: 10 }}>
{listSubBidang.map((item, index) => ( {listSubBidang?.map((item, index) => (
<Grid key={index}> <Grid key={index}>
<Grid.Col span={2} style={{ alignItems: "center" }}> <Grid.Col span={2} style={{ alignItems: "center" }}>
{item.icon} <Ionicons
name="chevron-forward-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
</Grid.Col> </Grid.Col>
<Grid.Col span={10}> <Grid.Col span={10}>
<TextCustom style={{ paddingLeft: 5 }}> <TextCustom style={{ paddingLeft: 5 }}>
{item.label} {item?.MasterSubBidangBisnis?.name || "-"}
</TextCustom> </TextCustom>
</Grid.Col> </Grid.Col>
</Grid> </Grid>
@@ -160,10 +184,7 @@ export default function Portofolio_Data() {
</Grid> </Grid>
<TextCustom style={{ paddingInline: 10 }}> <TextCustom style={{ paddingInline: 10 }}>
Lorem ipsum, dolor sit amet consectetur adipisicing elit. {(data && data?.deskripsi) || "-"}
Doloremque, alias perspiciatis quis enim eos facilis sit est?
Doloremque, rerum. Cumque error asperiores harum temporibus
cupiditate ullam, id quibusdam! Harum, rerum!
</TextCustom> </TextCustom>
</View> </View>
</StackCustom> </StackCustom>

View File

@@ -21,7 +21,7 @@ export default function Portofolio_MenuDrawerSection({
<MenuDrawerDynamicGrid <MenuDrawerDynamicGrid
data={drawerItems} data={drawerItems}
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,21 +0,0 @@
import { Spacing, StackCustom } from "@/components";
import Portofolio_BusinessLocation from "./BusinessLocationSection";
import Portofolio_Data from "./DataPortofolio";
import Portofolio_SocialMediaSection from "./SocialMediaSection";
import Portofolio_ButtonDelete from "./ButtonDelete";
export default function PorfofolioSection({
setShowDeleteAlert,
}: {
setShowDeleteAlert: (value: boolean) => void;
}) {
return (
<StackCustom>
<Portofolio_Data />
<Portofolio_BusinessLocation />
<Portofolio_SocialMediaSection />
<Portofolio_ButtonDelete setShowDeleteAlert={setShowDeleteAlert}/>
<Spacing/>
</StackCustom>
);
}

View File

@@ -3,10 +3,10 @@ import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value"; import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { Ionicons } from "@expo/vector-icons"; import { Ionicons } from "@expo/vector-icons";
export default function Portofolio_SocialMediaSection() { export default function Portofolio_SocialMediaSection({ data }: { data: any }) {
const listData = [ const listData = [
{ {
label: "Facebook ku bagas", label: data && data?.facebook ? data.facebook : "-",
icon: ( icon: (
<Ionicons <Ionicons
name="logo-facebook" name="logo-facebook"
@@ -16,7 +16,7 @@ export default function Portofolio_SocialMediaSection() {
), ),
}, },
{ {
label: "Tiktok ku bagas", label: data && data?.tiktok ? data.tiktok : "-",
icon: ( icon: (
<Ionicons <Ionicons
name="logo-tiktok" name="logo-tiktok"
@@ -26,7 +26,7 @@ export default function Portofolio_SocialMediaSection() {
), ),
}, },
{ {
label: "Instagram ku bagas", label: data && data?.instagram ? data.instagram : "-",
icon: ( icon: (
<Ionicons <Ionicons
name="logo-instagram" name="logo-instagram"
@@ -36,7 +36,7 @@ export default function Portofolio_SocialMediaSection() {
), ),
}, },
{ {
label: "Twitter ku bagas", label: data && data?.twitter ? data.twitter : "-",
icon: ( icon: (
<Ionicons <Ionicons
name="logo-twitter" name="logo-twitter"
@@ -46,7 +46,7 @@ export default function Portofolio_SocialMediaSection() {
), ),
}, },
{ {
label: "Youtube ku bagas", label: data && data?.youtube ? data.youtube : "-",
icon: ( icon: (
<Ionicons <Ionicons
name="logo-youtube" name="logo-youtube"

View File

@@ -1,4 +1,5 @@
import { AvatarCustom, ClickableCustom } from "@/components"; import { AvatarComp, ClickableCustom } from "@/components";
import API_STRORAGE from "@/constants/base-url-api-strorage";
import { AccentColor } from "@/constants/color-palet"; import { AccentColor } from "@/constants/color-palet";
import DUMMY_IMAGE from "@/constants/dummy-image-value"; import DUMMY_IMAGE from "@/constants/dummy-image-value";
import { router } from "expo-router"; import { router } from "expo-router";
@@ -12,8 +13,6 @@ const AvatarAndBackground = ({
imageId: string; imageId: string;
}) => { }) => {
return ( return (
// console.log("backgroundId", backgroundId),
// console.log("imageId", imageId),
<View style={styles.container}> <View style={styles.container}>
{/* Background Image */} {/* Background Image */}
<ClickableCustom <ClickableCustom
@@ -24,24 +23,19 @@ const AvatarAndBackground = ({
}} }}
> >
<ImageBackground <ImageBackground
source={DUMMY_IMAGE.background} source={
backgroundId
? { uri: API_STRORAGE.GET({ fileId: backgroundId }) }
: DUMMY_IMAGE.background
}
style={styles.backgroundImage} style={styles.backgroundImage}
resizeMode="cover" resizeMode="cover"
/> />
</ClickableCustom> </ClickableCustom>
{/* Avatar yang sedikit keluar */} {/* Avatar yang sedikit keluar */}
<View style={styles.avatarOverlap}> <View style={styles.avatarOverlap}>
<ClickableCustom <AvatarComp size="lg" fileId={imageId} />
onPress={() => {
router.navigate(
`/(application)/(image)/preview-image/${imageId}`
);
}}
>
<AvatarCustom source={DUMMY_IMAGE.avatar} size="lg" />
</ClickableCustom>
</View> </View>
</View> </View>
); );

View File

@@ -0,0 +1,45 @@
import { BaseBox, CenterCustom, Spacing, TextCustom } from "@/components";
import { router } from "expo-router";
import { View } from "react-native";
import Portofolio_BoxView from "../Portofolio/BoxPortofolioView";
export default function Profile_PortofolioSection({
profileId,
data,
}: {
profileId: string;
data: any[];
}) {
return (
<>
<BaseBox>
<View>
<TextCustom bold size="large" align="center">
Portofolio
</TextCustom>
<Spacing />
{data?.length > 0 ? (
data?.map((item: any, index) => (
<Portofolio_BoxView key={index} data={item} />
))
) : (
<CenterCustom>
<TextCustom>Tidak ada portofolio</TextCustom>
</CenterCustom>
)}
</View>
{data?.length > 0 && (
<TextCustom
bold
align="right"
onPress={() => router.push(`/portofolio/${profileId}/list`)}
>
Lihat semua
</TextCustom>
)}
</BaseBox>
</>
);
}

View File

@@ -1,16 +1,12 @@
import { BaseBox, Grid, Spacing, TextCustom } from "@/components"; import { BaseBox, Grid, Spacing, TextCustom } from "@/components";
import { MainColor } from "@/constants/color-palet";
import { ICON_SIZE_SMALL } from "@/constants/constans-value"; import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { IProfile } from "@/types/Type-Profile";
import { FontAwesome5, Ionicons } from "@expo/vector-icons"; import { FontAwesome5, Ionicons } from "@expo/vector-icons";
import { router, useLocalSearchParams } from "expo-router"; import _ from "lodash";
import { View } from "react-native"; import { View } from "react-native";
import AvatarAndBackground from "./AvatarAndBackground"; import AvatarAndBackground from "./AvatarAndBackground";
import { IProfile } from "@/types/Type-Profile";
import _ from "lodash";
export default function ProfileSection({ data }: { data: IProfile }) { export default function ProfileSection({ data }: { data: IProfile }) {
const { id } = useLocalSearchParams();
const listData = [ const listData = [
{ {
icon: ( icon: (
@@ -47,9 +43,6 @@ export default function ProfileSection({ data }: { data: IProfile }) {
return ( return (
<> <>
<BaseBox> <BaseBox>
{/* <TextCustom>
{JSON.stringify(data.imageBackgroundId, null, 2)}
</TextCustom> */}
<AvatarAndBackground <AvatarAndBackground
backgroundId={data?.imageBackgroundId as any} backgroundId={data?.imageBackgroundId as any}
imageId={data?.imageId as any} imageId={data?.imageId as any}
@@ -85,57 +78,7 @@ export default function ProfileSection({ data }: { data: IProfile }) {
))} ))}
</BaseBox> </BaseBox>
<BaseBox>
<View>
<TextCustom bold size="large" align="center">
Portofolio
</TextCustom>
<Spacing />
{Array.from({ length: 2 }).map((_, index) => (
<BaseBox
key={index}
style={{ backgroundColor: MainColor.darkblue }}
onPress={() => {
console.log("press to Portofolio");
router.push(`/portofolio/${id}`);
}}
>
<Grid>
<Grid.Col
span={10}
style={{ justifyContent: "center", backgroundColor: "" }}
>
<TextCustom bold size="large" truncate={1}>
Nama usaha portofolio
</TextCustom>
<TextCustom size="small" color="yellow">
#id-porofolio12345
</TextCustom>
</Grid.Col>
<Grid.Col
span={2}
style={{ alignItems: "flex-end", justifyContent: "center" }}
>
<Ionicons
name="caret-forward"
size={ICON_SIZE_SMALL}
color="white"
/>
</Grid.Col>
</Grid>
</BaseBox>
))}
</View>
<TextCustom
bold
align="right"
onPress={() => router.push(`/portofolio/${id}/list`)}
>
Lihat semua
</TextCustom>
</BaseBox>
</> </>
); );
} }

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,45 +1,95 @@
import { import {
BadgeCustom,
BoxWithHeaderSection, BoxWithHeaderSection,
ButtonCustom, ButtonCustom,
CenterCustom,
Spacing, Spacing,
StackCustom, StackCustom,
TextCustom TextCustom,
} from "@/components"; } from "@/components";
import { RadioCustom, RadioGroup } from "@/components/Radio/RadioCustom"; import { RadioCustom, RadioGroup } from "@/components/Radio/RadioCustom";
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";
import { apiVotingVote } from "@/service/api-client/api-voting";
import { useAuth } from "@/hooks/use-auth";
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,
});
console.log("[RES VOTE]", response);
} 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 label={item?.value} value={item?.id} />
</View>
))}
</RadioGroup>
</StackCustom>
<ButtonCustom onPress={() => console.log("vote")}>Vote</ButtonCustom> <ButtonCustom disabled={value === ""} onPress={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,140 @@
import { apiConfig } from "../api-config";
export async function apiEventCreate(data: any) {
try {
const response = await apiConfig.post(`/mobile/event`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiEventGetByStatus({
id,
status,
}: {
id: string;
status: string;
}) {
try {
const response = await apiConfig.get(`/mobile/event/${id}/${status}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiEventGetOne({ id }: { id: string }) {
try {
const response = await apiConfig.get(`/mobile/event/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiEventUpdateStatus({
id,
status,
}: {
id: string;
status: "draft" | "review" | "publish" | "reject";
}) {
try {
const response = await apiConfig.put(`/mobile/event/${id}/${status}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiEventUpdateData({
id,
data,
}: {
id: string;
data: any;
}) {
try {
const response = await apiConfig.put(`/mobile/event/${id}`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiEventDelete({ id }: { id: string }) {
try {
const response = await apiConfig.delete(`/mobile/event/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiEventGetAll({
category,
userId,
}: {
category?: "beranda" | "contribution" | "all-history" | "my-history";
userId?: string;
}) {
try {
const categoryEvent = category ? `?category=${category}` : "";
const userIdCreator = userId ? `&userId=${userId}` : "";
const response = await apiConfig.get(
`/mobile/event${categoryEvent}${userIdCreator}`
);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiEventJoin({
id,
userId,
}: {
id: string;
userId?: string;
}) {
try {
const response = await apiConfig.post(`/mobile/event/${id}/participants`, {
userId: userId,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiEventListOfParticipants({ id }: { id?: string }) {
try {
const response = await apiConfig.get(`/mobile/event/${id}/participants`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiEventCheckParticipants({
id,
userId,
}: {
id: string;
userId?: string;
}) {
try {
const selectUserId = userId ? `?userId=${userId}` : "";
const response = await apiConfig.get(
`/mobile/event/${id}/check-participants${selectUserId}`
);
return response.data;
} catch (error) {
throw error;
}
}

View File

@@ -0,0 +1,45 @@
import axios from "axios";
import { API_BASE_URL } from "../api-config";
const url = `${API_BASE_URL}/mobile/file`;
export async function apiFileUpload({
token,
formData,
}: {
token: string;
formData: FormData;
}) {
try {
const response = await axios.post(url, formData, {
headers: {
"Content-Type": "multipart/form-data",
Authorization: `Bearer ${token}`,
},
// timeout: 30000,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiFileDelete({
token,
id,
}: {
token: string;
id: string;
}) {
try {
const response = await axios.delete(`${url}/${id}`, {
headers: {
Authorization: `Bearer ${token}`,
},
// timeout: 30000,
});
return response.data;
} catch (error) {
throw error;
}
}

View File

@@ -0,0 +1,104 @@
import { apiConfig } from "../api-config";
export async function apiJobCreate(data: any) {
try {
const response = await apiConfig.post(`/mobile/job`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiJobGetByStatus({
authorId,
status,
}: {
authorId: string;
status: string;
}) {
try {
const response = await apiConfig.get(`/mobile/job/${authorId}/${status}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiJobGetOne({ id }: { id: string }) {
try {
const response = await apiConfig.get(`/mobile/job/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiJobUpdateStatus({
id,
status,
}: {
id: string;
status: "draft" | "review" | "publish" | "reject";
}) {
try {
const response = await apiConfig.put(`/mobile/job/${id}/${status}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiJobDelete({ id }: { id: string }) {
try {
const response = await apiConfig.delete(`/mobile/job/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiJobGetAll({
search,
category,
authorId,
}: {
search?: string;
category: "archive" | "beranda";
authorId?: string;
}) {
try {
let categoryText = category ? `?category=${category}` : "";
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;
} catch (error) {
throw error;
}
}
export async function apiJobUpdateData({
id,
data,
category,
}: {
id: string;
data: any;
category: "edit" | "archive";
}) {
try {
const categoryJob = category ? `?category=${category}` : "";
const response = await apiConfig.put(`/mobile/job/${id}${categoryJob}`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}

View File

@@ -0,0 +1,31 @@
import { apiConfig } from "../api-config";
export async function apiMasterBidangBisnis() {
try {
const response = await apiConfig.get(`/master/bidang-bisnis`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiMasterSubBidangBisnis({ id }: { id?: string }) {
try {
const selectBidangId = id ? `/${id}` : "";
const response = await apiConfig.get(
`/master/sub-bidang-bisnis${selectBidangId}`
);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiMasterEventType() {
try {
const response = await apiConfig.get(`/mobile/master/event-type`);
return response.data;
} catch (error) {
throw error;
}
}

View File

@@ -0,0 +1,66 @@
import { apiConfig } from "../api-config";
export async function apiPortofolioCreate({ data }: { data: any }) {
try {
const response = await apiConfig.post(`/mobile/portofolio`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiGetPortofolio({ id }: { id: string }) {
try {
const response = await apiConfig.get(`/mobile/portofolio?id=${id}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiGetOnePortofolio({ id }: { id: string }) {
try {
const response = await apiConfig.get(`/mobile/portofolio/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiDeletePortofolio({ id }: { id: string }) {
try {
const response = await apiConfig.delete(`/mobile/portofolio/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiUpdatePortofolio({
id,
data,
category,
}: {
id: string;
data: any;
category: "detail" | "medsos" | "logo";
}) {
try {
const response = await apiConfig.put(
`/mobile/portofolio/${id}?category=${category}`,
{
data: data,
}
);
return response.data;
} catch (error) {
throw error;
}
}

View File

@@ -15,11 +15,13 @@ export async function apiProfile({ id }: { id: string }) {
export async function apiUpdateProfile({ export async function apiUpdateProfile({
id, id,
data, data,
category,
}: { }: {
id: string; id: string;
data: any; data: any;
category: "profile" | "photo" | "background";
}) { }) {
const response = await apiConfig.put(`/mobile/profile/${id}`, { const response = await apiConfig.put(`/mobile/profile/${id}?category=${category}`, {
data: data, data: data,
}); });
return response.data; return response.data;

View File

@@ -1,6 +1,12 @@
import { apiConfig } from "../api-config"; import { apiConfig } from "../api-config";
export async function apiUser(id: string) { export async function apiUser(id: string) {
const response = await apiConfig.get(`/user/${id}`); const response = await apiConfig.get(`/mobile/user/${id}`);
return response.data; return response.data;
} }
export async function apiAllUser({ search }: { search: string }) {
const response = await apiConfig.get(`/mobile/user?search=${search}`);
return response.data;
}

View File

@@ -0,0 +1,8 @@
import { apiConfig } from "../api-config";
export async function apiValidationEmail({ email }: { email: string }) {
const response = await apiConfig.post(`/mobile/validate/email`, {
data: email,
});
return response.data;
}

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