Compare commits

...

2 Commits

Author SHA1 Message Date
f750d158be Integrasi Map Business
Add:
 components/Map/MapSelected.tsx
        components/_ShareComponent/GridTwoView.tsx
        service/api-client/api-maps.ts
utils/openInDeviceMaps.ts

Fix:
 modified:   app/(application)/(user)/maps/[id]/edit.tsx
        modified:   app/(application)/(user)/maps/create.tsx
        modified:   app/(application)/(user)/maps/index.tsx
        modified:   app/(application)/(user)/portofolio/[id]/index.tsx
        modified:   components/Map/MapCustom.tsx
        modified:   screens/Portofolio/BusinessLocationSection.tsx
        modified:   screens/Portofolio/DataPortofolio.tsx
        modified:   screens/Portofolio/ListPage.tsx

### No issue
2025-10-13 17:46:47 +08:00
0e7b29bb15 Integrasi API Donation
Fix:
- (application)/(user)/donation/[id]/(news)/[news]/edit-news
- (application)/(user)/donation/[id]/(news)/[news]/index
- (application)/(user)/donation/[id]/(news)/add-news
- (application)/(user)/donation/[id]/(news)/list-of-news
- (application)/(user)/donation/[id]/(news)/recap-of-news
- (application)/(user)/donation/[id]/infromation-fundrising
- service/api-client/api-donation

### No Issue
2025-10-09 17:00:04 +08:00
19 changed files with 1083 additions and 102 deletions

View File

@@ -1,3 +1,4 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
ButtonCenteredOnly, ButtonCenteredOnly,
ButtonCustom, ButtonCustom,
@@ -9,17 +10,120 @@ import {
TextInputCustom, TextInputCustom,
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import { router } from "expo-router"; import API_STRORAGE from "@/constants/base-url-api-strorage";
import DIRECTORY_ID from "@/constants/directory-id";
import {
apiDonationGetNewsById,
apiDonationUpdateNews,
} from "@/service/api-client/api-donation";
import { uploadFileService } from "@/service/upload-service";
import pickFile, { IFileData } from "@/utils/pickFile";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
export default function DonationEditNews() { export default function DonationEditNews() {
const { news } = useLocalSearchParams();
const [data, setData] = useState<any>(null);
const [image, setImage] = useState<IFileData | null>(null);
const [isLoading, setLoading] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [news])
);
const onLoadData = async () => {
try {
const response = await apiDonationGetNewsById({
id: news as string,
category: "get-one",
});
setData(response.data);
} catch (error) {
console.log("[ERROR]", error);
}
};
const handlerSubmitUpdate = async () => {
let newData;
if (!data.title || !data.deskripsi) {
Toast.show({
type: "error",
text1: "Judul dan deskripsi harus diisi",
});
return;
}
try {
setLoading(true);
newData = {
title: data?.title,
deskripsi: data?.deskripsi,
};
if (image && image?.uri) {
const uploadNewImage = await uploadFileService({
dirId: DIRECTORY_ID.donasi_kabar,
imageUri: image?.uri,
});
newData = {
title: data?.title,
deskripsi: data?.deskripsi,
newImageId: uploadNewImage.data.id,
};
}
const response = await apiDonationUpdateNews({
id: news as string,
data: newData,
});
if (!response.success) {
Toast.show({
type: "error",
text1: "Gagal mengupdate berita",
});
return;
}
Toast.show({
type: "success",
text1: "Berita berhasil diperbarui",
});
router.back();
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoading(false);
}
};
return ( return (
<ViewWrapper> <ViewWrapper>
<StackCustom gap={"xs"}> <StackCustom gap={"xs"}>
<InformationBox text="Upload gambar bersifat opsional untuk melengkapi kabar terkait donasi Anda." /> <InformationBox text="Upload gambar bersifat opsional untuk melengkapi kabar terkait donasi Anda." />
<LandscapeFrameUploaded /> <LandscapeFrameUploaded
image={
image
? image.uri
: data && data.imageId
? API_STRORAGE.GET({ fileId: data.imageId })
: undefined
}
/>
<ButtonCenteredOnly <ButtonCenteredOnly
onPress={() => { onPress={() => {
router.push("/(application)/(image)/take-picture/123"); pickFile({
allowedType: "image",
setImageUri(file) {
setImage(file);
},
});
}} }}
icon="upload" icon="upload"
> >
@@ -30,6 +134,8 @@ export default function DonationEditNews() {
label="Judul Berita" label="Judul Berita"
placeholder="Masukan judul berita" placeholder="Masukan judul berita"
required required
value={data?.title}
onChangeText={(value) => setData({ ...data, title: value })}
/> />
<TextAreaCustom <TextAreaCustom
label="Deskripsi Berita" label="Deskripsi Berita"
@@ -37,12 +143,16 @@ export default function DonationEditNews() {
required required
showCount showCount
maxLength={1000} maxLength={1000}
value={data?.deskripsi}
onChangeText={(value) => setData({ ...data, deskripsi: value })}
/> />
<Spacing /> <Spacing />
<ButtonCustom <ButtonCustom
disabled={!data?.title || !data?.deskripsi}
isLoading={isLoading}
onPress={() => { onPress={() => {
router.back(); handlerSubmitUpdate();
}} }}
> >
Update Update

View File

@@ -14,9 +14,11 @@ import {
import { IconEdit } from "@/components/_Icon"; import { IconEdit } from "@/components/_Icon";
import { IconTrash } from "@/components/_Icon/IconTrash"; import { IconTrash } from "@/components/_Icon/IconTrash";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import { apiDonationGetNewsById } from "@/service/api-client/api-donation"; import {
apiDonationDeleteNews,
apiDonationGetNewsById,
} from "@/service/api-client/api-donation";
import { formatChatTime } from "@/utils/formatChatTime"; import { formatChatTime } from "@/utils/formatChatTime";
import dayjs from "dayjs";
import { import {
router, router,
Stack, Stack,
@@ -24,6 +26,7 @@ import {
useLocalSearchParams, useLocalSearchParams,
} from "expo-router"; } from "expo-router";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import Toast from "react-native-toast-message";
export default function DonationNews() { export default function DonationNews() {
const { user } = useAuth(); const { user } = useAuth();
@@ -57,7 +60,7 @@ export default function DonationNews() {
title: "Detail Kabar", title: "Detail Kabar",
headerLeft: () => <BackButton />, headerLeft: () => <BackButton />,
headerRight: () => headerRight: () =>
user?.id === data && data?.authorId && ( user?.id === data?.authorId && (
<DotButton onPress={() => setOpenDrawer(true)} /> <DotButton onPress={() => setOpenDrawer(true)} />
), ),
}} }}
@@ -97,7 +100,7 @@ export default function DonationNews() {
{ {
icon: <IconTrash />, icon: <IconTrash />,
label: "Hapus Berita", label: "Hapus Berita",
path: `/donation/[id]/(news)/${news}/edit-news` as any, path: "",
color: "red", color: "red",
}, },
]} ]}
@@ -109,7 +112,23 @@ export default function DonationNews() {
message: "Apakah Anda yakin ingin menghapus berita ini?", message: "Apakah Anda yakin ingin menghapus berita ini?",
textLeft: "Batal", textLeft: "Batal",
textRight: "Hapus", textRight: "Hapus",
onPressRight: () => { onPressRight: async () => {
const response = await apiDonationDeleteNews({
id: news as string,
});
if (!response.success) {
Toast.show({
type: "error",
text1: "Gagal menghapus berita",
});
return;
}
Toast.show({
type: "success",
text1: "Berita berhasil dihapus",
});
router.back(); router.back();
}, },
}); });

View File

@@ -19,7 +19,6 @@ import Toast from "react-native-toast-message";
export default function DonationAddNews() { export default function DonationAddNews() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
console.log("[ID]", id);
const [data, setData] = useState({ const [data, setData] = useState({
title: "", title: "",
deskripsi: "", deskripsi: "",
@@ -48,8 +47,6 @@ export default function DonationAddNews() {
data: newData, data: newData,
}); });
console.log("[RESPONSE]", JSON.stringify(response, null, 2));
if (!response.success) { if (!response.success) {
Toast.show({ Toast.show({
type: "error", type: "error",

View File

@@ -12,7 +12,6 @@ import {
import { IconPlus } from "@/components/_Icon"; import { IconPlus } from "@/components/_Icon";
import { apiDonationGetNewsById } from "@/service/api-client/api-donation"; import { apiDonationGetNewsById } from "@/service/api-client/api-donation";
import { formatChatTime } from "@/utils/formatChatTime"; import { formatChatTime } from "@/utils/formatChatTime";
import dayjs from "dayjs";
import { import {
router, router,
Stack, Stack,
@@ -42,7 +41,6 @@ export default function DonationRecapOfNews() {
category: "get-all", category: "get-all",
}); });
console.log("[RESPONSE LIST]", JSON.stringify(response, null, 2));
setList(response.data); setList(response.data);
} catch (error) { } catch (error) {
console.log("[ERROR]", error); console.log("[ERROR]", error);

View File

@@ -13,7 +13,6 @@ import {
import { IconPlus } from "@/components/_Icon"; import { IconPlus } from "@/components/_Icon";
import { apiDonationGetNewsById } from "@/service/api-client/api-donation"; import { apiDonationGetNewsById } from "@/service/api-client/api-donation";
import { formatChatTime } from "@/utils/formatChatTime"; import { formatChatTime } from "@/utils/formatChatTime";
import dayjs from "dayjs";
import { import {
router, router,
Stack, Stack,
@@ -25,7 +24,6 @@ import { useCallback, useState } from "react";
export default function DonationRecapOfNews() { export default function DonationRecapOfNews() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
console.log("[ID]", id);
const [openDrawer, setOpenDrawer] = useState(false); const [openDrawer, setOpenDrawer] = useState(false);
const [list, setList] = useState<any[] | null>(null); const [list, setList] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState<boolean>(false); const [loadList, setLoadList] = useState<boolean>(false);
@@ -44,7 +42,6 @@ export default function DonationRecapOfNews() {
category: "get-all", category: "get-all",
}); });
console.log("[RESPONSE LIST]", JSON.stringify(response, null, 2));
setList(response.data); setList(response.data);
} catch (error) { } catch (error) {
console.log("[ERROR]", error); console.log("[ERROR]", error);

View File

@@ -32,10 +32,6 @@ export default function DonationInformationFunrising() {
try { try {
setLoadList(true); setLoadList(true);
const response = await apiDonationFundrising({ id: id as string }); const response = await apiDonationFundrising({ id: id as string });
console.log(
"[RES GET FUNDRISING]",
JSON.stringify(response.data, null, 2)
);
setData(response?.data?.user); setData(response?.data?.user);
setList(response?.data?.donasi); setList(response?.data?.donasi);
@@ -78,7 +74,7 @@ export default function DonationInformationFunrising() {
{loadList ? ( {loadList ? (
<LoaderCustom /> <LoaderCustom />
) : _.isEmpty(list) ? ( ) : _.isEmpty(list) ? (
<TextCustom>Belum ada data</TextCustom> <TextCustom align="center" color="gray" size="small">Belum ada data</TextCustom>
) : ( ) : (
list?.map((item: any, index: number) => ( list?.map((item: any, index: number) => (
<Donation_BoxPublish key={index} id={item?.id} data={item} /> <Donation_BoxPublish key={index} id={item?.id} data={item} />

View File

@@ -1,54 +1,226 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { import {
BaseBox,
BoxButtonOnFooter, BoxButtonOnFooter,
ButtonCenteredOnly, ButtonCenteredOnly,
ButtonCustom, ButtonCustom,
InformationBox, InformationBox,
LandscapeFrameUploaded, LandscapeFrameUploaded,
MapCustom,
Spacing, Spacing,
TextInputCustom, TextInputCustom,
ViewWrapper ViewWrapper,
} from "@/components"; } from "@/components";
import { router, useLocalSearchParams } from "expo-router"; import API_IMAGE from "@/constants/api-storage";
import DIRECTORY_ID from "@/constants/directory-id";
import { apiMapsGetOne, apiMapsUpdate } from "@/service/api-client/api-maps";
import { uploadFileService } from "@/service/upload-service";
import pickFile, { IFileData } from "@/utils/pickFile";
import { router, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, useState } from "react";
import { StyleSheet, View } from "react-native";
import MapView, { LatLng, Marker } from "react-native-maps";
import Toast from "react-native-toast-message";
const defaultRegion = {
latitude: -8.737109,
longitude: 115.1756897,
latitudeDelta: 0.1,
longitudeDelta: 0.1,
};
export default function MapsEdit() { export default function MapsEdit() {
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const [data, setData] = useState<any | null>({
id: "",
namePin: "",
latitude: "",
longitude: "",
imageId: "",
});
const [selectedLocation, setSelectedLocation] = useState<LatLng | null>(null);
const [image, setImage] = useState<IFileData | null>(null);
const [isLoading, setLoading] = useState(false);
useFocusEffect(
useCallback(() => {
onLoadData();
}, [id])
);
const onLoadData = async () => {
try {
const response = await apiMapsGetOne({ id: id as string });
if (response.success) {
setData({
id: response.data.id,
namePin: response.data.namePin,
latitude: response.data.latitude,
longitude: response.data.longitude,
imageId: response.data.imageId,
});
}
} catch (error) {
console.log("[ERROR]", error);
}
};
const handleMapPress = (event: any) => {
const { latitude, longitude } = event.nativeEvent.coordinate;
const location = { latitude, longitude };
setSelectedLocation(location);
};
const handleSubmit = async () => {
let newData: any;
if (!data.namePin) {
Toast.show({
type: "error",
text1: "Nama pin harus diisi",
});
return;
}
newData = {
namePin: data?.namePin,
latitude: selectedLocation?.latitude || data?.latitude,
longitude: selectedLocation?.longitude || data?.longitude,
};
try {
setLoading(true);
if (image) {
const responseUpload = await uploadFileService({
dirId: DIRECTORY_ID.map_image,
imageUri: image?.uri,
});
if (!responseUpload?.data?.id) {
Toast.show({
type: "error",
text1: "Gagal mengunggah gambar",
});
return;
}
const imageId = responseUpload?.data?.id;
newData = {
namePin: data?.namePin,
latitude: selectedLocation?.latitude,
longitude: selectedLocation?.longitude,
newImageId: imageId,
};
}
const responseUpdate = await apiMapsUpdate({
id: data?.id,
data: newData,
});
if (!responseUpdate.success) {
Toast.show({
type: "error",
text1: "Gagal mengupdate map",
});
return;
}
Toast.show({
type: "success",
text1: "Map berhasil diupdate",
});
router.back();
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoading(false);
}
};
const buttonFooter = ( const buttonFooter = (
<BoxButtonOnFooter> <BoxButtonOnFooter>
<ButtonCustom <ButtonCustom
onPress={() => { disabled={!data.namePin}
console.log(`Simpan maps ${id}`); onPress={handleSubmit}
router.back() isLoading={isLoading}
}}
> >
Simpan Update
</ButtonCustom> </ButtonCustom>
</BoxButtonOnFooter> </BoxButtonOnFooter>
); );
return ( return (
<ViewWrapper footerComponent={buttonFooter}> <ViewWrapper footerComponent={buttonFooter}>
<InformationBox text="Tentukan lokasi pin map dengan menekan pada map." /> <InformationBox text="Tentukan lokasi pin map dengan menekan pada map." />
<BaseBox> <View style={[styles.container, { height: 400 }]}>
<MapCustom /> <MapView
</BaseBox> style={styles.map}
initialRegion={
data?.latitude && data?.longitude
? {
latitude: data?.latitude,
longitude: data?.longitude,
latitudeDelta: 0.1,
longitudeDelta: 0.1,
}
: defaultRegion
}
onPress={handleMapPress}
showsUserLocation={true}
showsMyLocationButton={true}
loadingEnabled={true}
loadingIndicatorColor="#666"
loadingBackgroundColor="#f0f0f0"
>
{selectedLocation ? (
<Marker
coordinate={selectedLocation}
title="Lokasi Dipilih"
description={`Lat: ${selectedLocation.latitude.toFixed(
6
)}, Lng: ${selectedLocation.longitude.toFixed(6)}`}
pinColor="red"
/>
) : (
<Marker
coordinate={defaultRegion}
title="Lokasi Dipilih"
description={`Lat: ${defaultRegion.latitude.toFixed(
6
)}, Lng: ${defaultRegion.longitude.toFixed(6)}`}
pinColor="red"
/>
)}
</MapView>
</View>
<TextInputCustom <TextInputCustom
required required
label="Nama Pin" label="Nama Pin"
placeholder="Masukkan nama pin maps" placeholder="Masukkan nama pin maps"
value={data?.namePin}
onChangeText={(value) => setData({ ...data, namePin: value })}
/> />
<Spacing /> <Spacing />
<InformationBox text="Upload foto lokasi bisnis anda untuk ditampilkan dalam detail maps." /> <InformationBox text="Upload foto lokasi bisnis anda untuk ditampilkan dalam detail maps." />
<LandscapeFrameUploaded /> <LandscapeFrameUploaded
image={
image
? image?.uri
: API_IMAGE.GET({ fileId: data?.imageId as string })
}
/>
<ButtonCenteredOnly <ButtonCenteredOnly
icon="upload" icon="upload"
onPress={() => { onPress={() => {
console.log("Upload foto "); pickFile({
router.navigate(`/take-picture/${id}`); allowedType: "image",
setImageUri(file) {
setImage(file);
},
});
}} }}
> >
Upload Upload
@@ -57,3 +229,16 @@ export default function MapsEdit() {
</ViewWrapper> </ViewWrapper>
); );
} }
const styles = StyleSheet.create({
container: {
width: "100%",
backgroundColor: "#f5f5f5",
overflow: "hidden",
borderRadius: 8,
marginBottom: 20,
},
map: {
flex: 1,
},
});

View File

@@ -6,21 +6,96 @@ import {
InformationBox, InformationBox,
LandscapeFrameUploaded, LandscapeFrameUploaded,
Spacing, Spacing,
TextCustom,
TextInputCustom, TextInputCustom,
ViewWrapper, ViewWrapper,
} from "@/components"; } from "@/components";
import MapSelected from "@/components/Map/MapSelected";
import DIRECTORY_ID from "@/constants/directory-id";
import { useAuth } from "@/hooks/use-auth";
import { apiMapsCreate } from "@/service/api-client/api-maps";
import { uploadFileService } from "@/service/upload-service";
import pickFile, { IFileData } from "@/utils/pickFile";
import { router, useLocalSearchParams } from "expo-router"; import { router, useLocalSearchParams } from "expo-router";
import { useState } from "react";
import { LatLng } from "react-native-maps";
import Toast from "react-native-toast-message";
export default function MapsCreate() { export default function MapsCreate() {
const { user } = useAuth();
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const [selectedLocation, setSelectedLocation] = useState<LatLng | null>(null);
const [name, setName] = useState<string>("");
const [image, setImage] = useState<IFileData | null>(null);
const [isLoading, setLoading] = useState(false);
const handleSubmit = async () => {
try {
setLoading(true);
let newData: any;
newData = {
authorId: user?.id,
portofolioId: id,
namePin: name,
latitude: selectedLocation?.latitude,
longitude: selectedLocation?.longitude,
};
if (image) {
const responseUpload = await uploadFileService({
dirId: DIRECTORY_ID.map_image,
imageUri: image?.uri,
});
if (!responseUpload?.data?.id) {
Toast.show({
type: "error",
text1: "Gagal mengunggah gambar",
});
return;
}
const imageId = responseUpload?.data?.id;
newData = {
authorId: user?.id,
portofolioId: id,
namePin: name,
latitude: selectedLocation?.latitude,
longitude: selectedLocation?.longitude,
imageId: imageId,
};
}
const response = await apiMapsCreate({
data: newData,
});
if (!response.success) {
Toast.show({
type: "error",
text1: "Gagal menambahkan map",
});
return;
}
Toast.show({
type: "success",
text1: "Map berhasil ditambahkan",
});
router.back();
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoading(false);
}
};
const buttonFooter = ( const buttonFooter = (
<BoxButtonOnFooter> <BoxButtonOnFooter>
<ButtonCustom <ButtonCustom
onPress={() => { isLoading={isLoading}
console.log(`Simpan maps ${id}`); disabled={!selectedLocation || name === ""}
router.replace(`/portofolio/${id}`); onPress={handleSubmit}
}}
> >
Simpan Simpan
</ButtonCustom> </ButtonCustom>
@@ -30,25 +105,34 @@ export default function MapsCreate() {
<ViewWrapper footerComponent={buttonFooter}> <ViewWrapper footerComponent={buttonFooter}>
<InformationBox text="Tentukan lokasi pin map dengan menekan pada map." /> <InformationBox text="Tentukan lokasi pin map dengan menekan pada map." />
<BaseBox style={{ height: 400 }}> <BaseBox>
<TextCustom>Maps Her</TextCustom> <MapSelected
selectedLocation={selectedLocation as any}
setSelectedLocation={setSelectedLocation}
/>
</BaseBox> </BaseBox>
<TextInputCustom <TextInputCustom
required required
label="Nama Pin" label="Nama Pin"
placeholder="Masukkan nama pin maps" placeholder="Masukkan nama pin maps"
value={name}
onChangeText={setName}
/> />
<Spacing height={50} /> <Spacing height={50} />
<InformationBox text="Upload foto lokasi bisnis anda untuk ditampilkan dalam detail maps." /> <InformationBox text="Upload foto lokasi bisnis anda untuk ditampilkan dalam detail maps." />
<LandscapeFrameUploaded /> <LandscapeFrameUploaded image={image?.uri} />
<ButtonCenteredOnly <ButtonCenteredOnly
icon="upload" icon="upload"
onPress={() => { onPress={() => {
console.log("Upload foto "); pickFile({
router.navigate(`/take-picture/${id}`); allowedType: "image",
setImageUri(file) {
setImage(file);
},
});
}} }}
> >
Upload Upload

View File

@@ -1,19 +1,234 @@
import { MapCustom, ViewWrapper } from "@/components"; import {
ButtonCustom,
DrawerCustom,
DummyLandscapeImage,
Grid,
Spacing,
StackCustom,
TextCustom,
ViewWrapper,
} from "@/components";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
import API_IMAGE from "@/constants/api-storage";
import { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { apiMapsGetAll } from "@/service/api-client/api-maps";
import { openInDeviceMaps } from "@/utils/openInDeviceMaps";
import { FontAwesome, Ionicons } from "@expo/vector-icons";
import { Image } from "expo-image";
import { router, useFocusEffect } from "expo-router";
import { useCallback, useState } from "react";
import { View } from "react-native"; import { View } from "react-native";
import MapView from "react-native-maps"; import MapView, { Marker } from "react-native-maps";
const defaultRegion = {
latitude: -8.737109,
longitude: 115.1756897,
latitudeDelta: 0.1,
longitudeDelta: 0.1,
height: 300,
};
export interface LocationItem {
id: string | number;
latitude: number;
longitude: number;
name: string;
imageId?: string;
}
export default function Maps() { export default function Maps() {
const [list, setList] = useState<any[] | null>(null);
const [loadList, setLoadList] = useState(false);
const [openDrawer, setOpenDrawer] = useState(false);
const [selected, setSelected] = useState({
id: "",
bidangBisnis: "",
nomorTelepon: "",
alamatBisnis: "",
namePin: "",
imageId: "",
portofolioId: "",
latitude: 0,
longitude: 0,
});
useFocusEffect(
useCallback(() => {
handlerLoadList();
}, [])
);
const handlerLoadList = async () => {
try {
setLoadList(true);
const response = await apiMapsGetAll();
if (response.success) {
setList(response.data);
}
} catch (error) {
console.log("[ERROR]", error);
} finally {
setLoadList(false);
}
};
return ( return (
<ViewWrapper style={{ paddingInline: 0, paddingBlock: 0 }}> <>
{/* <MapCustom height={"100%"} /> */} <ViewWrapper style={{ paddingInline: 0, paddingBlock: 0 }}>
<View style={{ flex: 1 }}> {/* <MapCustom height={"100%"} /> */}
<MapView <View style={{ flex: 1 }}>
style={{ {loadList ? (
width: "100%", <MapView
height: "100%", initialRegion={defaultRegion}
}} style={{
/> width: "100%",
</View> height: "100%",
</ViewWrapper> }}
/>
) : (
<MapView
initialRegion={defaultRegion}
style={{
width: "100%",
height: "100%",
}}
>
{list?.map((item: any, index: number) => {
return (
<Marker
key={item?.id}
coordinate={{
latitude: item?.latitude,
longitude: item?.longitude,
}}
title={item?.namePin}
onPress={() => {
setOpenDrawer(true);
setSelected({
id: item?.id,
bidangBisnis:
item?.Portofolio?.MasterBidangBisnis?.name,
nomorTelepon: item?.Portofolio?.tlpn,
alamatBisnis: item?.Portofolio?.alamatKantor,
namePin: item?.namePin,
imageId: item?.imageId,
portofolioId: item?.Portofolio?.id,
latitude: item?.latitude,
longitude: item?.longitude,
});
}}
// Gunakan gambar kustom jika tersedia
>
<View style={{}}>
<Image
source={{
uri: API_IMAGE.GET({
fileId: item?.Portofolio?.logoId,
}),
}}
style={{
width: 30,
height: 30,
borderRadius: 100,
borderWidth: 1,
}}
/>
</View>
</Marker>
);
})}
</MapView>
)}
</View>
</ViewWrapper>
<DrawerCustom
isVisible={openDrawer}
closeDrawer={() => setOpenDrawer(false)}
height={"auto"}
>
<DummyLandscapeImage height={200} imageId={selected.imageId} />
<Spacing />
<StackCustom gap={"xs"}>
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
<FontAwesome
name="building-o"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={<TextCustom>{selected.namePin}</TextCustom>}
/>
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
<Ionicons
name="list-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={<TextCustom>{selected.bidangBisnis}</TextCustom>}
/>
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
<Ionicons
name="call-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={<TextCustom>{selected.nomorTelepon}</TextCustom>}
/>
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
<Ionicons
name="location-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={<TextCustom>{selected.alamatBisnis}</TextCustom>}
/>
<Grid>
<Grid.Col span={6} style={{ paddingRight: 10 }}>
<ButtonCustom
onPress={() => {
setOpenDrawer(false);
router.push(`/portofolio/${selected.portofolioId}`);
}}
>
Detail
</ButtonCustom>
</Grid.Col>
<Grid.Col span={6} style={{ paddingLeft: 10 }}>
<ButtonCustom
onPress={() => {
openInDeviceMaps({
latitude: selected.latitude,
longitude: selected.longitude,
title: selected.namePin,
});
}}
>
Buka Maps
</ButtonCustom>
</Grid.Col>
</Grid>
</StackCustom>
</DrawerCustom>
</>
); );
} }

View File

@@ -1,8 +1,18 @@
/* eslint-disable react-hooks/exhaustive-deps */ /* eslint-disable react-hooks/exhaustive-deps */
import { DrawerCustom, LoaderCustom, Spacing, StackCustom } from "@/components"; import {
ButtonCustom,
DrawerCustom,
DummyLandscapeImage,
LoaderCustom,
Spacing,
StackCustom,
TextCustom,
} from "@/components";
import LeftButtonCustom from "@/components/Button/BackButton"; import LeftButtonCustom from "@/components/Button/BackButton";
import GridTwoView from "@/components/_ShareComponent/GridTwoView";
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 { ICON_SIZE_SMALL } from "@/constants/constans-value";
import { useAuth } from "@/hooks/use-auth"; import { useAuth } from "@/hooks/use-auth";
import Portofolio_BusinessLocation from "@/screens/Portofolio/BusinessLocationSection"; import Portofolio_BusinessLocation from "@/screens/Portofolio/BusinessLocationSection";
import Portofolio_ButtonDelete from "@/screens/Portofolio/ButtonDelete"; import Portofolio_ButtonDelete from "@/screens/Portofolio/ButtonDelete";
@@ -13,19 +23,20 @@ import Portofolio_SocialMediaSection from "@/screens/Portofolio/SocialMediaSecti
import { apiGetOnePortofolio } from "@/service/api-client/api-portofolio"; import { apiGetOnePortofolio } from "@/service/api-client/api-portofolio";
import { apiUser } from "@/service/api-client/api-user"; 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 { openInDeviceMaps } from "@/utils/openInDeviceMaps";
import { FontAwesome, Ionicons } from "@expo/vector-icons";
import { Stack, useFocusEffect, useLocalSearchParams } from "expo-router"; import { Stack, useFocusEffect, useLocalSearchParams } from "expo-router";
import { useCallback, 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 { user } = useAuth();
const { id } = useLocalSearchParams(); const { id } = useLocalSearchParams();
const [isDrawerOpen, setIsDrawerOpen] = useState(false); const [isDrawerOpen, setIsDrawerOpen] = useState(false);
const [isLoadingDelete, setIsLoadingDelete] = useState(false); const [isLoadingDelete, setIsLoadingDelete] = useState(false);
const [data, setData] = useState<any>(); const [data, setData] = useState<any>();
const [profileId, setProfileId] = useState<any>(); const [profileId, setProfileId] = useState<any>();
const [openDrawerLocation, setOpenDrawerLocation] = useState(false);
const { user } = useAuth();
const openDrawer = () => { const openDrawer = () => {
setIsDrawerOpen(true); setIsDrawerOpen(true);
@@ -43,19 +54,13 @@ export default function Portofolio() {
async function onLoadData(id: string) { async function onLoadData(id: string) {
const response = await apiGetOnePortofolio({ id: id }); const response = await apiGetOnePortofolio({ id: id });
console.log(
"[PROFILE ID]>>",
JSON.stringify(response.data.Profile.id, null, 2)
);
setData(response.data); setData(response.data);
} }
const onLoadUserByToken = async () => { const onLoadUserByToken = async () => {
const response = await apiUser(user?.id as string); 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); setProfileId(response?.data?.Profile?.id);
}; };
@@ -89,7 +94,11 @@ export default function Portofolio() {
data={data} data={data}
listSubBidang={data?.Portofolio_BidangDanSubBidangBisnis as any[]} listSubBidang={data?.Portofolio_BidangDanSubBidangBisnis as any[]}
/> />
<Portofolio_BusinessLocation /> <Portofolio_BusinessLocation
data={data?.BusinessMaps}
imageId={data?.logoId}
setOpenDrawerLocation={setOpenDrawerLocation}
/>
<Portofolio_SocialMediaSection <Portofolio_SocialMediaSection
data={data?.Portofolio_MediaSosial} data={data?.Portofolio_MediaSosial}
/> />
@@ -110,10 +119,93 @@ export default function Portofolio() {
height={"auto"} height={"auto"}
> >
<Portofolio_MenuDrawerSection <Portofolio_MenuDrawerSection
drawerItems={drawerItemsPortofolio({ id: id as string })} drawerItems={drawerItemsPortofolio({
id: id as string,
maps: data?.BusinessMaps,
})}
setIsDrawerOpen={setIsDrawerOpen} setIsDrawerOpen={setIsDrawerOpen}
/> />
</DrawerCustom> </DrawerCustom>
{/* Drawer Lokasi */}
<DrawerCustom
isVisible={openDrawerLocation}
closeDrawer={() => setOpenDrawerLocation(false)}
height={"auto"}
>
<DummyLandscapeImage
height={200}
imageId={data?.BusinessMaps?.imageId}
/>
<Spacing />
<StackCustom gap={"xs"}>
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
<FontAwesome
name="building-o"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={<TextCustom>{data?.BusinessMaps?.namePin}</TextCustom>}
/>
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
<Ionicons
name="list-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={
<TextCustom>{data?.MasterBidangBisnis?.name}</TextCustom>
}
/>
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
<Ionicons
name="call-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={<TextCustom>{data?.tlpn}</TextCustom>}
/>
<GridTwoView
spanLeft={2}
spanRight={10}
leftIcon={
<Ionicons
name="location-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
}
rightIcon={<TextCustom>{data?.alamatKantor}</TextCustom>}
/>
<Spacing />
<ButtonCustom
onPress={() => {
openInDeviceMaps({
latitude: data?.BusinessMaps?.latitude,
longitude: data?.BusinessMaps?.longitude,
title: data?.BusinessMaps?.namePin,
});
}}
>
Buka Maps
</ButtonCustom>
</StackCustom>
</DrawerCustom>
</> </>
); );
} }

View File

@@ -1,5 +1,7 @@
// components/MapComponent.js // components/MapComponent.js
import API_IMAGE from "@/constants/api-storage";
import { Image } from "expo-image";
import React from "react"; import React from "react";
import { DimensionValue, StyleSheet, View } from "react-native"; import { DimensionValue, StyleSheet, View } from "react-native";
import MapView, { Marker } from "react-native-maps"; import MapView, { Marker } from "react-native-maps";
@@ -10,6 +12,9 @@ interface MapComponentProps {
latitudeDelta?: number; latitudeDelta?: number;
longitudeDelta?: number; longitudeDelta?: number;
height?: DimensionValue; height?: DimensionValue;
namePin?: string;
imageId?: string;
onPress?: () => void;
} }
const MapCustom = ({ const MapCustom = ({
@@ -18,6 +23,9 @@ const MapCustom = ({
latitudeDelta = 0.1, latitudeDelta = 0.1,
longitudeDelta = 0.1, longitudeDelta = 0.1,
height = 300, height = 300,
namePin = "Bali",
imageId,
onPress,
}: MapComponentProps) => { }: MapComponentProps) => {
const initialRegion = { const initialRegion = {
latitude, latitude,
@@ -40,9 +48,22 @@ const MapCustom = ({
latitude, latitude,
longitude, longitude,
}} }}
title="Bali" title={namePin}
description="Badung, Bali, Indonesia" onPress={onPress}
/> // Gunakan gambar kustom jika tersedia
>
<View style={{}}>
<Image
source={{ uri: API_IMAGE.GET({ fileId: imageId as string }) }}
style={{
width: 30,
height: 30,
borderRadius: 100,
borderWidth: 1,
}}
/>
</View>
</Marker>
</MapView> </MapView>
</View> </View>
); );

View File

@@ -0,0 +1,79 @@
import React from "react";
import { StyleSheet, View } from "react-native";
import MapView, { LatLng, Marker } from "react-native-maps";
interface MapSelectedProps {
initialRegion?: {
latitude?: number;
longitude?: number;
latitudeDelta?: number;
longitudeDelta?: number;
};
onLocationSelect?: (location: LatLng) => void;
height?: number; // Opsional: tinggi peta dalam piksel
selectedLocation: LatLng;
setSelectedLocation: (location: LatLng) => void;
}
const MapSelected: React.FC<MapSelectedProps> = ({
initialRegion,
onLocationSelect,
selectedLocation,
setSelectedLocation,
height = 400, // Default height: 400
}) => {
const handleMapPress = (event: any) => {
const { latitude, longitude } = event.nativeEvent.coordinate;
const location = { latitude, longitude };
setSelectedLocation(location);
onLocationSelect?.(location);
};
// Default region sesuai permintaan
const defaultRegion = {
latitude: -8.737109,
longitude: 115.1756897,
latitudeDelta: 0.1,
longitudeDelta: 0.1,
};
return (
<View style={[styles.container, { height }]}>
<MapView
style={styles.map}
initialRegion={(initialRegion as any) || defaultRegion}
onPress={handleMapPress}
showsUserLocation={true}
showsMyLocationButton={true}
loadingEnabled={true}
loadingIndicatorColor="#666"
loadingBackgroundColor="#f0f0f0"
>
{selectedLocation && (
<Marker
coordinate={selectedLocation}
title="Lokasi Dipilih"
description={`Lat: ${selectedLocation.latitude.toFixed(
6
)}, Lng: ${selectedLocation.longitude.toFixed(6)}`}
pinColor="red"
/>
)}
</MapView>
</View>
);
};
const styles = StyleSheet.create({
container: {
width: "100%",
backgroundColor: "#f5f5f5",
overflow: "hidden",
borderRadius: 8,
},
map: {
flex: 1,
},
});
export default MapSelected;

View File

@@ -0,0 +1,29 @@
import { StyleProp, ViewStyle } from "react-native";
import Grid from "../Grid/GridCustom";
export default function GridTwoView({
spanLeft = 6,
spanRight = 6,
leftIcon,
rightIcon,
styleLeft,
styleRight,
}: {
spanLeft?: number;
spanRight?: number;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
styleLeft?: StyleProp<ViewStyle>;
styleRight?: StyleProp<ViewStyle>;
}) {
return (
<Grid containerStyle={{ marginBottom: 0 }}>
<Grid.Col span={spanLeft} style={{ justifyContent: "center" }}>
{leftIcon}
</Grid.Col>
<Grid.Col span={spanRight} style={{ justifyContent: "center" }}>
{rightIcon}
</Grid.Col>
</Grid>
);
}

View File

@@ -1,12 +1,45 @@
import { BaseBox, MapCustom, StackCustom, TextCustom } from "@/components"; import {
BaseBox,
MapCustom,
StackCustom,
TextCustom
} from "@/components";
export default function Portofolio_BusinessLocation() { export default function Portofolio_BusinessLocation({
data,
imageId,
setOpenDrawerLocation,
}: {
data: any;
imageId?: string;
setOpenDrawerLocation: (value: boolean) => void;
}) {
return ( return (
<> <>
<BaseBox> <BaseBox style={{ height: !data ? 200 : "auto" }}>
<StackCustom> <StackCustom>
<TextCustom bold>Lokasi Bisnis</TextCustom> <TextCustom bold>Lokasi Bisnis</TextCustom>
<MapCustom /> {!data ? (
<TextCustom
style={{ paddingTop: 50 }}
align="center"
color="gray"
size={"small"}
bold
>
Lokasi bisnis belum ditambahkan
</TextCustom>
) : (
<MapCustom
latitude={data?.latitude}
longitude={data?.longitude}
namePin={data?.namePin}
imageId={imageId}
onPress={() => {
setOpenDrawerLocation(true);
}}
/>
)}
</StackCustom> </StackCustom>
</BaseBox> </BaseBox>
</> </>

View File

@@ -41,7 +41,11 @@ export default function Portofolio_Data({
}, },
{ {
icon: ( icon: (
<Ionicons name="home-outline" size={ICON_SIZE_SMALL} color="white" /> <Ionicons
name="location-outline"
size={ICON_SIZE_SMALL}
color="white"
/>
), ),
label: data && data?.alamatKantor ? data.alamatKantor : "-", label: data && data?.alamatKantor ? data.alamatKantor : "-",
}, },
@@ -89,8 +93,6 @@ export default function Portofolio_Data({
// }, // },
// ]; // ];
// console.log("List Sub Bidang >>", JSON.stringify(listSubBidang, null, 2));
return ( return (
<> <>
<BaseBox> <BaseBox>

View File

@@ -1,9 +1,19 @@
import { IMenuDrawerItem } from "@/components/_Interface/types"; import { IMenuDrawerItem } from "@/components/_Interface/types";
import { AccentColor } from "@/constants/color-palet"; import { AccentColor } from "@/constants/color-palet";
import { ICON_SIZE_MEDIUM } from "@/constants/constans-value"; import { ICON_SIZE_MEDIUM } from "@/constants/constans-value";
import { Ionicons, FontAwesome5, FontAwesome, Fontisto } from "@expo/vector-icons"; import {
FontAwesome,
Fontisto,
Ionicons
} from "@expo/vector-icons";
export const drawerItemsPortofolio = ({ id }: { id: string }): IMenuDrawerItem[] => [ export const drawerItemsPortofolio = ({
id,
maps,
}: {
id: string;
maps: any;
}): IMenuDrawerItem[] => [
{ {
icon: ( icon: (
<Ionicons <Ionicons
@@ -45,18 +55,20 @@ export const drawerItemsPortofolio = ({ id }: { id: string }): IMenuDrawerItem[]
color={AccentColor.white} color={AccentColor.white}
/> />
), ),
label: "Edit Map", label: `${!maps ? "Tambah" : "Edit"} Map`,
path: `/(application)/maps/${id}/edit`, path: !maps
}, ? `/(application)/maps/create?id=${id}`
{ : `/(application)/maps/${maps?.id}/edit`,
icon: (
<FontAwesome5
name="map-pin"
size={ICON_SIZE_MEDIUM}
color={AccentColor.white}
/>
),
label: "Custom Pin Map",
path: `/(application)/maps/${id}/custom-pin`,
}, },
// {
// icon: (
// <FontAwesome5
// name="map-pin"
// size={ICON_SIZE_MEDIUM}
// color={AccentColor.white}
// />
// ),
// label: "Custom Pin Map",
// path: `/(application)/maps/${id}/custom-pin`,
// },
]; ];

View File

@@ -213,11 +213,45 @@ export async function apiDonationCreateNews({
} }
} }
export async function apiDonationGetNewsById({ id , category}: { id: string , category: "get-all" | "get-one"}) { export async function apiDonationGetNewsById({
id,
category,
}: {
id: string;
category: "get-all" | "get-one";
}) {
try { try {
const response = await apiConfig.get(`/mobile/donation/${id}/news?category=${category}`); const response = await apiConfig.get(
`/mobile/donation/${id}/news?category=${category}`
);
return response.data; return response.data;
} catch (error) { } catch (error) {
throw error; throw error;
} }
} }
export async function apiDonationUpdateNews({
id,
data,
}: {
id: string;
data: any;
}) {
try {
const response = await apiConfig.put(`/mobile/donation/${id}/news`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiDonationDeleteNews({ id }: { id: string }) {
try {
const response = await apiConfig.delete(`/mobile/donation/${id}/news`);
return response.data;
} catch (error) {
throw error;
}
}

View File

@@ -0,0 +1,41 @@
import { apiConfig } from "../api-config";
export async function apiMapsCreate({ data }: { data: any }) {
try {
const response = await apiConfig.post(`/mobile/maps`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}
export async function apiMapsGetAll() {
try {
const response = await apiConfig.get("/mobile/maps");
return response.data;
} catch (error) {
throw error;
}
}
export async function apiMapsGetOne({ id }: { id: string }) {
try {
const response = await apiConfig.get(`/mobile/maps/${id}`);
return response.data;
} catch (error) {
throw error;
}
}
export async function apiMapsUpdate({ id, data }: { id: string; data: any }) {
try {
const response = await apiConfig.put(`/mobile/maps/${id}`, {
data: data,
});
return response.data;
} catch (error) {
throw error;
}
}

37
utils/openInDeviceMaps.ts Normal file
View File

@@ -0,0 +1,37 @@
import { Platform, Linking, Alert } from "react-native";
export const openInDeviceMaps = async ({
latitude,
longitude,
title = "Lokasi",
}: {
latitude: number;
longitude: number;
title?: string;
}) => {
let url = "";
if (Platform.OS === "ios") {
// Apple Maps
url = `maps://?q=${encodeURIComponent(title)}&ll=${latitude},${longitude}`;
} else {
// Android: Google Maps
url = `geo:${latitude},${longitude}?q=${latitude},${longitude}(${encodeURIComponent(
title
)})`;
}
try {
const canOpen = await Linking.canOpenURL(url);
if (canOpen) {
await Linking.openURL(url);
} else {
// Fallback ke web
const webUrl = `https://www.google.com/maps/search/?api=1&query=${latitude},${longitude}`;
await Linking.openURL(webUrl);
}
} catch (error) {
console.warn("Gagal membuka Maps:", error);
Alert.alert("Error", "Tidak dapat membuka aplikasi Maps.");
}
};