fix-maps dan bug #58
33
QWEN.md
33
QWEN.md
@@ -10,7 +10,7 @@ HIPMI Mobile is a cross-platform mobile application built with Expo and React Na
|
||||
- **Architecture**: File-based routing with Expo Router
|
||||
- **State Management**: Context API (AuthContext)
|
||||
- **UI Components**: React Native Paper, custom components
|
||||
- **Maps Integration**: Mapbox Maps for React Native
|
||||
- **Maps Integration**: Maplibre Maps for React Native (`@maplibre/maplibre-react-native` v10.4.2)
|
||||
- **Push Notifications**: React Native Firebase Messaging
|
||||
- **Build System**: Metro bundler
|
||||
- **Package Manager**: Bun
|
||||
@@ -381,8 +381,8 @@ apiConfig.interceptors.request.use(async (config) => {
|
||||
- Push Notifications (FCM)
|
||||
- Configured for both iOS and Android
|
||||
|
||||
### Mapbox
|
||||
- Map integration via `@rnmapbox/maps`
|
||||
### Maplibre
|
||||
- Map integration via `@maplibre/maplibre-react-native`
|
||||
- Location permissions configured
|
||||
|
||||
### Deep Linking
|
||||
@@ -475,10 +475,34 @@ rm -rf node_modules bun.lock
|
||||
bun install
|
||||
```
|
||||
|
||||
### iOS Maplibre Crash Fix
|
||||
|
||||
When using Maplibre MapView on iOS, prevent "Attempt to recycle a mounted view" crash:
|
||||
|
||||
1. **Always render PointAnnotation** (not conditional)
|
||||
2. **Use opacity for visibility** instead of conditional rendering
|
||||
3. **Avoid key prop changes** that force remounting
|
||||
|
||||
```typescript
|
||||
// ✅ GOOD: Stable PointAnnotation
|
||||
<PointAnnotation
|
||||
coordinate={annotationCoordinate} // Always rendered
|
||||
...
|
||||
>
|
||||
<View style={{ opacity: selectedLocation ? 1 : 0 }}>
|
||||
<SelectedLocationMarker />
|
||||
</View>
|
||||
</PointAnnotation>
|
||||
|
||||
// ❌ BAD: Conditional rendering causes crash
|
||||
{selectedLocation && (
|
||||
<PointAnnotation coordinate={selectedLocation} ... />
|
||||
)}
|
||||
```
|
||||
|
||||
## Documentation Files
|
||||
|
||||
- `docs/CHANGE_LOG.md` - Change log for recent updates
|
||||
- `docs/COMMIT_NOTES.md` - Commit notes and guidelines
|
||||
- `docs/hipmi-note.md` - Build and deployment notes
|
||||
- `docs/prompt-for-qwen-code.md` - Development prompts and patterns
|
||||
|
||||
@@ -488,3 +512,4 @@ bun install
|
||||
- [React Native Documentation](https://reactnative.dev/)
|
||||
- [Expo Router Documentation](https://docs.expo.dev/router/introduction/)
|
||||
- [TypeScript Documentation](https://www.typescriptlang.org/docs/)
|
||||
- [Maplibre React Native](https://github.com/maplibre/maplibre-react-native)
|
||||
|
||||
@@ -100,8 +100,8 @@ packagingOptions {
|
||||
applicationId 'com.bip.hipmimobileapp'
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 4
|
||||
versionName "1.0.1"
|
||||
versionCode 1
|
||||
versionName "1.0.2"
|
||||
|
||||
buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ require("dotenv").config();
|
||||
export default {
|
||||
name: "HIPMI Badung Connect",
|
||||
slug: "hipmi-mobile",
|
||||
version: "1.0.1",
|
||||
version: "1.0.2",
|
||||
orientation: "portrait",
|
||||
icon: "./assets/images/icon.png",
|
||||
scheme: "hipmimobile",
|
||||
@@ -21,7 +21,7 @@ export default {
|
||||
"Aplikasi membutuhkan akses lokasi untuk menampilkan peta.",
|
||||
},
|
||||
associatedDomains: ["applinks:cld-dkr-staging-hipmi.wibudev.com"],
|
||||
buildNumber: "21",
|
||||
buildNumber: "1",
|
||||
},
|
||||
|
||||
android: {
|
||||
@@ -32,7 +32,7 @@ export default {
|
||||
},
|
||||
edgeToEdgeEnabled: true,
|
||||
package: "com.bip.hipmimobileapp",
|
||||
versionCode: 4,
|
||||
versionCode: 1,
|
||||
// softwareKeyboardLayoutMode: 'resize', // option: untuk mengatur keyboard pada room chst collaboration
|
||||
intentFilters: [
|
||||
{
|
||||
|
||||
@@ -1,244 +1,5 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCenteredOnly,
|
||||
ButtonCustom,
|
||||
InformationBox,
|
||||
LandscapeFrameUploaded,
|
||||
Spacing,
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
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";
|
||||
import { Maps_ScreenMapsEdit } from "@/screens/Maps/ScreenMapsEdit";
|
||||
|
||||
const defaultRegion = {
|
||||
latitude: -8.737109,
|
||||
longitude: 115.1756897,
|
||||
latitudeDelta: 0.1,
|
||||
longitudeDelta: 0.1,
|
||||
};
|
||||
export default function MapsEdit() {
|
||||
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 = (
|
||||
<BoxButtonOnFooter>
|
||||
<ButtonCustom
|
||||
disabled={!data.namePin}
|
||||
onPress={handleSubmit}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Update
|
||||
</ButtonCustom>
|
||||
</BoxButtonOnFooter>
|
||||
);
|
||||
|
||||
return (
|
||||
<ViewWrapper footerComponent={buttonFooter}>
|
||||
<InformationBox text="Tentukan lokasi pin map dengan menekan pada map." />
|
||||
|
||||
<View style={[styles.container, { height: 400 }]}>
|
||||
<MapView
|
||||
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
|
||||
required
|
||||
label="Nama Pin"
|
||||
placeholder="Masukkan nama pin maps"
|
||||
value={data?.namePin}
|
||||
onChangeText={(value) => setData({ ...data, namePin: value })}
|
||||
/>
|
||||
|
||||
<Spacing />
|
||||
|
||||
<InformationBox text="Upload foto lokasi bisnis anda untuk ditampilkan dalam detail maps." />
|
||||
<LandscapeFrameUploaded
|
||||
image={
|
||||
image
|
||||
? image?.uri
|
||||
: API_IMAGE.GET({ fileId: data?.imageId as string })
|
||||
}
|
||||
/>
|
||||
<ButtonCenteredOnly
|
||||
icon="upload"
|
||||
onPress={() => {
|
||||
pickFile({
|
||||
allowedType: "image",
|
||||
setImageUri(file) {
|
||||
setImage(file);
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Upload
|
||||
</ButtonCenteredOnly>
|
||||
<Spacing height={50} />
|
||||
</ViewWrapper>
|
||||
);
|
||||
return <Maps_ScreenMapsEdit />;
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
width: "100%",
|
||||
backgroundColor: "#f5f5f5",
|
||||
overflow: "hidden",
|
||||
borderRadius: 8,
|
||||
marginBottom: 20,
|
||||
},
|
||||
map: {
|
||||
flex: 1,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,143 +1,9 @@
|
||||
import {
|
||||
BaseBox,
|
||||
BoxButtonOnFooter,
|
||||
ButtonCenteredOnly,
|
||||
ButtonCustom,
|
||||
InformationBox,
|
||||
LandscapeFrameUploaded,
|
||||
Spacing,
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
} 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 { useState } from "react";
|
||||
import { LatLng } from "react-native-maps";
|
||||
import Toast from "react-native-toast-message";
|
||||
import Maps_ScreenMapsCreate from "@/screens/Maps/ScreenMapsCreate";
|
||||
|
||||
export default function MapsCreate() {
|
||||
const { user } = useAuth();
|
||||
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 = (
|
||||
<BoxButtonOnFooter>
|
||||
<ButtonCustom
|
||||
isLoading={isLoading}
|
||||
disabled={!selectedLocation || name === ""}
|
||||
onPress={handleSubmit}
|
||||
>
|
||||
Simpan
|
||||
</ButtonCustom>
|
||||
</BoxButtonOnFooter>
|
||||
);
|
||||
return (
|
||||
<ViewWrapper footerComponent={buttonFooter}>
|
||||
<InformationBox text="Tentukan lokasi pin map dengan menekan pada map." />
|
||||
|
||||
<BaseBox>
|
||||
<MapSelected
|
||||
selectedLocation={selectedLocation as any}
|
||||
setSelectedLocation={setSelectedLocation}
|
||||
/>
|
||||
</BaseBox>
|
||||
|
||||
<TextInputCustom
|
||||
required
|
||||
label="Nama Pin"
|
||||
placeholder="Masukkan nama pin maps"
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
/>
|
||||
|
||||
<Spacing height={50} />
|
||||
|
||||
<InformationBox text="Upload foto lokasi bisnis anda untuk ditampilkan dalam detail maps." />
|
||||
<LandscapeFrameUploaded image={image?.uri} />
|
||||
<ButtonCenteredOnly
|
||||
icon="upload"
|
||||
onPress={() => {
|
||||
pickFile({
|
||||
allowedType: "image",
|
||||
setImageUri(file) {
|
||||
setImage(file);
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Upload
|
||||
</ButtonCenteredOnly>
|
||||
<Spacing height={50} />
|
||||
</ViewWrapper>
|
||||
<>
|
||||
<Maps_ScreenMapsCreate />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
110
components/Map/MapSelectedPlatform.tsx
Normal file
110
components/Map/MapSelectedPlatform.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { Platform } from "react-native";
|
||||
import MapSelected from "./MapSelected";
|
||||
import MapSelectedV2 from "./MapSelectedV2";
|
||||
import { LatLng } from "react-native-maps";
|
||||
|
||||
/**
|
||||
* Props untuk komponen MapSelectedPlatform
|
||||
* Mendukung kedua format koordinat (LatLng untuk iOS, [number, number] untuk Android)
|
||||
*/
|
||||
export interface MapSelectedPlatformProps {
|
||||
/** Region awal kamera */
|
||||
initialRegion?: {
|
||||
latitude?: number;
|
||||
longitude?: number;
|
||||
latitudeDelta?: number;
|
||||
longitudeDelta?: number;
|
||||
};
|
||||
|
||||
/** Lokasi yang dipilih (support kedua format) */
|
||||
selectedLocation: LatLng | [number, number] | null;
|
||||
|
||||
/** Callback ketika lokasi dipilih */
|
||||
onLocationSelect: (location: LatLng | [number, number]) => void;
|
||||
|
||||
/** Tinggi peta dalam pixels (default: 400) */
|
||||
height?: number;
|
||||
|
||||
/** Tampilkan lokasi user (default: true) */
|
||||
showUserLocation?: boolean;
|
||||
|
||||
/** Tampilkan tombol my location (default: true) */
|
||||
showsMyLocationButton?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Komponen Map yang otomatis memilih implementasi berdasarkan platform
|
||||
*
|
||||
* Platform Strategy:
|
||||
* - **iOS**: Menggunakan react-native-maps (MapSelected)
|
||||
* - **Android**: Menggunakan @maplibre/maplibre-react-native (MapSelectedV2)
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <MapSelectedPlatform
|
||||
* selectedLocation={selectedLocation}
|
||||
* onLocationSelect={setSelectedLocation}
|
||||
* height={300}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export function MapSelectedPlatform({
|
||||
initialRegion,
|
||||
selectedLocation,
|
||||
onLocationSelect,
|
||||
height = 400,
|
||||
showUserLocation = true,
|
||||
showsMyLocationButton = true,
|
||||
}: MapSelectedPlatformProps) {
|
||||
// iOS: Gunakan react-native-maps
|
||||
// if (Platform.OS === "ios") {
|
||||
// return (
|
||||
// <MapSelected
|
||||
// initialRegion={initialRegion}
|
||||
// selectedLocation={(selectedLocation as LatLng) || { latitude: 0, longitude: 0 }}
|
||||
// setSelectedLocation={(location: LatLng) => {
|
||||
// onLocationSelect(location);
|
||||
// }}
|
||||
// height={height}
|
||||
// />
|
||||
// );
|
||||
// }
|
||||
|
||||
// Android: Gunakan MapLibre
|
||||
// Konversi dari LatLng ke [longitude, latitude] jika perlu
|
||||
const androidLocation: [number, number] | undefined = selectedLocation
|
||||
? isLatLng(selectedLocation)
|
||||
? [selectedLocation.longitude, selectedLocation.latitude]
|
||||
: selectedLocation
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<MapSelectedV2
|
||||
selectedLocation={androidLocation}
|
||||
onLocationSelect={(location: [number, number]) => {
|
||||
// Konversi dari [longitude, latitude] ke LatLng untuk konsistensi
|
||||
const latLng: LatLng = {
|
||||
latitude: location[1],
|
||||
longitude: location[0],
|
||||
};
|
||||
onLocationSelect(latLng);
|
||||
}}
|
||||
height={height}
|
||||
// showUserLocation={showUserLocation}
|
||||
// showsMyLocationButton={showsMyLocationButton}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard untuk mengecek apakah object adalah LatLng
|
||||
*/
|
||||
function isLatLng(location: any): location is LatLng {
|
||||
return (
|
||||
location &&
|
||||
typeof location.latitude === "number" &&
|
||||
typeof location.longitude === "number"
|
||||
);
|
||||
}
|
||||
|
||||
export default MapSelectedPlatform;
|
||||
150
components/Map/MapSelectedV2.tsx
Normal file
150
components/Map/MapSelectedV2.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import React, { useCallback, useRef, useEffect, useState } from "react";
|
||||
import { StyleSheet, View, ActivityIndicator } from "react-native";
|
||||
import {
|
||||
MapView,
|
||||
Camera,
|
||||
PointAnnotation,
|
||||
} from "@maplibre/maplibre-react-native";
|
||||
import * as Location from "expo-location";
|
||||
|
||||
const MAP_STYLE = "https://tiles.openfreemap.org/styles/liberty";
|
||||
const DEBOUNCE_MS = 800;
|
||||
|
||||
interface Props {
|
||||
selectedLocation?: [number, number];
|
||||
onLocationSelect?: (location: [number, number]) => void;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
export function MapSelectedV2({
|
||||
selectedLocation,
|
||||
onLocationSelect,
|
||||
height = 400,
|
||||
}: Props) {
|
||||
const lastTapRef = useRef<number>(0);
|
||||
const cameraRef = useRef<any>(null);
|
||||
|
||||
const [userLocation, setUserLocation] = useState<[number, number] | null>(
|
||||
null,
|
||||
);
|
||||
const [isLoadingLocation, setIsLoadingLocation] = useState(true);
|
||||
|
||||
// ✅ Ambil lokasi user saat pertama mount
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const { status } = await Location.requestForegroundPermissionsAsync();
|
||||
if (status !== "granted") {
|
||||
console.log("Permission lokasi ditolak");
|
||||
setIsLoadingLocation(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const location = await Location.getCurrentPositionAsync({
|
||||
accuracy: Location.Accuracy.Balanced,
|
||||
});
|
||||
|
||||
const coords: [number, number] = [
|
||||
location.coords.longitude,
|
||||
location.coords.latitude,
|
||||
];
|
||||
|
||||
setUserLocation(coords);
|
||||
|
||||
// ✅ Fly ke posisi user jika belum ada selectedLocation
|
||||
if (!selectedLocation && cameraRef.current) {
|
||||
cameraRef.current.flyTo(coords, 1000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Gagal ambil lokasi:", error);
|
||||
} finally {
|
||||
setIsLoadingLocation(false);
|
||||
}
|
||||
})();
|
||||
}, [isLoadingLocation]);
|
||||
|
||||
const handleMapPress = useCallback(
|
||||
(event: any) => {
|
||||
const now = Date.now();
|
||||
if (now - lastTapRef.current < DEBOUNCE_MS) return;
|
||||
lastTapRef.current = now;
|
||||
|
||||
const coords = event?.geometry?.coordinates;
|
||||
if (!coords || coords.length < 2) return;
|
||||
|
||||
onLocationSelect?.([coords[0], coords[1]]);
|
||||
},
|
||||
[onLocationSelect],
|
||||
);
|
||||
|
||||
// Center awal kamera:
|
||||
// 1. Jika ada selectedLocation → pakai itu
|
||||
// 2. Jika ada userLocation → pakai itu
|
||||
// 3. Fallback → Bali
|
||||
const initialCenter: [number, number] = selectedLocation ??
|
||||
userLocation ?? [115.1756897, -8.737109];
|
||||
|
||||
return (
|
||||
<View style={{ height, width: "100%" }}>
|
||||
{/* Loading indicator saat fetch lokasi */}
|
||||
{isLoadingLocation && (
|
||||
<View style={styles.loadingOverlay}>
|
||||
<ActivityIndicator size="small" color="#0a1f44" />
|
||||
</View>
|
||||
)}
|
||||
|
||||
<MapView
|
||||
style={StyleSheet.absoluteFillObject}
|
||||
mapStyle={MAP_STYLE}
|
||||
onPress={handleMapPress}
|
||||
logoEnabled={false}
|
||||
>
|
||||
<Camera
|
||||
ref={cameraRef}
|
||||
defaultSettings={{
|
||||
centerCoordinate: initialCenter,
|
||||
zoomLevel: 14,
|
||||
}}
|
||||
/>
|
||||
|
||||
{selectedLocation && (
|
||||
<PointAnnotation
|
||||
id="selected-location"
|
||||
key="selected-location"
|
||||
coordinate={selectedLocation}
|
||||
>
|
||||
<View style={styles.dot} />
|
||||
</PointAnnotation>
|
||||
)}
|
||||
</MapView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
dot: {
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: 10,
|
||||
backgroundColor: "#0a1f44",
|
||||
borderWidth: 2,
|
||||
borderColor: "#fff",
|
||||
},
|
||||
loadingOverlay: {
|
||||
position: "absolute",
|
||||
top: 10,
|
||||
alignSelf: "center",
|
||||
zIndex: 10,
|
||||
backgroundColor: "#fff",
|
||||
borderRadius: 20,
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 6,
|
||||
elevation: 4,
|
||||
shadowColor: "#000",
|
||||
shadowOffset: { width: 0, height: 2 },
|
||||
shadowOpacity: 0.15,
|
||||
shadowRadius: 4,
|
||||
},
|
||||
});
|
||||
|
||||
export default MapSelectedV2;
|
||||
@@ -111,10 +111,12 @@ Jika tidak ada props page maka tambahkan props page dan default page: "1" ( stri
|
||||
Jika butuh refrensi FlatList bisa lihat pada file components/_ShareComponent/NewWrapper.tsx
|
||||
|
||||
<!-- Create Box -->
|
||||
File Utama: screens/Admin/Investment/ScreenInvestmentStatus.tsx
|
||||
Folder tujuan: screens/Admin/Investment
|
||||
Reffrensi: screens/Admin/Donation/BoxDonationStatus.tsx
|
||||
Buatkan box component baru pada file "File Utama" di bagian renderItem agar lebih rapi buat file baru dengan nama BoxInvestmentStatus.tsx
|
||||
File Utama: app/(application)/(user)/maps/[id]/edit.tsx
|
||||
Folder tujuan: screens/Maps
|
||||
Nama file utama: ScreenMapsEdit.tsx
|
||||
Nama function utama: Maps_ScreenMapsEdit
|
||||
|
||||
Buatkan file baru pada "Folder tujuan" dengan nama "Nama file utama" dan ubah nama function menjadi "Nama function utama" kemudian clean code, import dan panggil function tersebut pada file "File source"
|
||||
|
||||
<!-- END Create Box -->
|
||||
|
||||
|
||||
@@ -163,6 +163,14 @@
|
||||
5A6E1555841A4B9D9D246E71 /* Remove signature files (Xcode workaround) */,
|
||||
0B4282049A4A4293821DF904 /* Remove signature files (Xcode workaround) */,
|
||||
CCCF75FD0B87410193A6B7DB /* Remove signature files (Xcode workaround) */,
|
||||
51F61F14096F4B7FBD9344A7 /* Remove signature files (Xcode workaround) */,
|
||||
60F3AE3AC4B24C2FA7FC8F10 /* Remove signature files (Xcode workaround) */,
|
||||
E188CB171C1B4A4DA64BC5C4 /* Remove signature files (Xcode workaround) */,
|
||||
90714A8C562E4676B84E6E07 /* Remove signature files (Xcode workaround) */,
|
||||
DB93AB500BC2459E8BAE3F74 /* Remove signature files (Xcode workaround) */,
|
||||
EEC6AC8AF9C04E91AA81C190 /* Remove signature files (Xcode workaround) */,
|
||||
D2BED766D85C4781B154BD69 /* Remove signature files (Xcode workaround) */,
|
||||
E01278D305D540D5B29ED50A /* Remove signature files (Xcode workaround) */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -635,6 +643,142 @@
|
||||
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
||||
";
|
||||
};
|
||||
51F61F14096F4B7FBD9344A7 /* Remove signature files (Xcode workaround) */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
name = "Remove signature files (Xcode workaround)";
|
||||
inputPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "
|
||||
echo \"Remove signature files (Xcode workaround)\";
|
||||
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
||||
";
|
||||
};
|
||||
60F3AE3AC4B24C2FA7FC8F10 /* Remove signature files (Xcode workaround) */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
name = "Remove signature files (Xcode workaround)";
|
||||
inputPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "
|
||||
echo \"Remove signature files (Xcode workaround)\";
|
||||
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
||||
";
|
||||
};
|
||||
E188CB171C1B4A4DA64BC5C4 /* Remove signature files (Xcode workaround) */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
name = "Remove signature files (Xcode workaround)";
|
||||
inputPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "
|
||||
echo \"Remove signature files (Xcode workaround)\";
|
||||
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
||||
";
|
||||
};
|
||||
90714A8C562E4676B84E6E07 /* Remove signature files (Xcode workaround) */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
name = "Remove signature files (Xcode workaround)";
|
||||
inputPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "
|
||||
echo \"Remove signature files (Xcode workaround)\";
|
||||
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
||||
";
|
||||
};
|
||||
DB93AB500BC2459E8BAE3F74 /* Remove signature files (Xcode workaround) */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
name = "Remove signature files (Xcode workaround)";
|
||||
inputPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "
|
||||
echo \"Remove signature files (Xcode workaround)\";
|
||||
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
||||
";
|
||||
};
|
||||
EEC6AC8AF9C04E91AA81C190 /* Remove signature files (Xcode workaround) */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
name = "Remove signature files (Xcode workaround)";
|
||||
inputPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "
|
||||
echo \"Remove signature files (Xcode workaround)\";
|
||||
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
||||
";
|
||||
};
|
||||
D2BED766D85C4781B154BD69 /* Remove signature files (Xcode workaround) */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
name = "Remove signature files (Xcode workaround)";
|
||||
inputPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "
|
||||
echo \"Remove signature files (Xcode workaround)\";
|
||||
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
||||
";
|
||||
};
|
||||
E01278D305D540D5B29ED50A /* Remove signature files (Xcode workaround) */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
name = "Remove signature files (Xcode workaround)";
|
||||
inputPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "
|
||||
echo \"Remove signature files (Xcode workaround)\";
|
||||
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
||||
";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.1</string>
|
||||
<string>1.0.2</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
@@ -39,7 +39,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>21</string>
|
||||
<string>1</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { apiJobGetAll } from "@/service/api-client/api-job";
|
||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||
|
||||
export default function Home_BottomFeatureSection() {
|
||||
const [listData, setListData] = useState<any>([]);
|
||||
const [listData, setListData] = useState<any[] | null>(null);
|
||||
|
||||
const onLoadData = async () => {
|
||||
try {
|
||||
@@ -17,7 +17,7 @@ export default function Home_BottomFeatureSection() {
|
||||
category: "beranda",
|
||||
});
|
||||
|
||||
// console.log("[DATA JOB]", JSON.stringify(response.data, null, 2));
|
||||
console.log("[DATA JOB]", JSON.stringify(response.data, null, 2));
|
||||
const result = response.data
|
||||
.sort(
|
||||
(a: any, b: any) =>
|
||||
@@ -36,7 +36,7 @@ export default function Home_BottomFeatureSection() {
|
||||
}, [])
|
||||
);
|
||||
|
||||
if (!listData || listData.length === 0) {
|
||||
if (listData === null) {
|
||||
return <CustomSkeleton height={200}/>
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ export default function Home_BottomFeatureSection() {
|
||||
|
||||
<View style={stylesHome.vacancyList}>
|
||||
{/* Vacancy Item 1 */}
|
||||
{listData.map((item: any, index: number) => (
|
||||
{listData?.map((item: any, index: number) => (
|
||||
<View style={stylesHome.vacancyItem} key={index}>
|
||||
<View style={stylesHome.vacancyDetails}>
|
||||
<TextCustom bold color="yellow" truncate size="large">
|
||||
|
||||
@@ -40,8 +40,12 @@ export default function DrawerMaps({
|
||||
closeDrawer={() => setOpenDrawer(false)}
|
||||
height={"auto"}
|
||||
>
|
||||
<DummyLandscapeImage height={200} imageId={selected.imageId} />
|
||||
<Spacing />
|
||||
{selected.imageId && (
|
||||
<>
|
||||
<DummyLandscapeImage height={200} imageId={selected.imageId} />
|
||||
<Spacing />
|
||||
</>
|
||||
)}
|
||||
<StackCustom gap={"xs"}>
|
||||
<GridTwoView
|
||||
spanLeft={2}
|
||||
|
||||
186
screens/Maps/ScreenMapsCreate.tsx
Normal file
186
screens/Maps/ScreenMapsCreate.tsx
Normal file
@@ -0,0 +1,186 @@
|
||||
import NewWrapper from "@/components/_ShareComponent/NewWrapper";
|
||||
import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCustom,
|
||||
InformationBox,
|
||||
BaseBox,
|
||||
Spacing,
|
||||
TextInputCustom,
|
||||
LandscapeFrameUploaded,
|
||||
ButtonCenteredOnly,
|
||||
} from "@/components";
|
||||
import { MapSelectedPlatform } from "@/components/Map/MapSelectedPlatform";
|
||||
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 { IFileData } from "@/utils/pickFile";
|
||||
import pickFile from "@/utils/pickFile";
|
||||
import { router, useLocalSearchParams } from "expo-router";
|
||||
import { useState } from "react";
|
||||
import { LatLng } from "react-native-maps";
|
||||
import Toast from "react-native-toast-message";
|
||||
|
||||
/**
|
||||
* Screen untuk create maps
|
||||
*
|
||||
* Fitur:
|
||||
* - Pilih lokasi dari map
|
||||
* - Input nama pin
|
||||
* - Upload foto lokasi
|
||||
* - Submit data maps
|
||||
*/
|
||||
export function Maps_ScreenMapsCreate() {
|
||||
const { user } = useAuth();
|
||||
const { id } = useLocalSearchParams();
|
||||
|
||||
// State management
|
||||
// Format: LatLng { latitude, longitude } untuk konsistensi
|
||||
const [selectedLocation, setSelectedLocation] = useState<LatLng | null>(null);
|
||||
const [name, setName] = useState<string>("");
|
||||
const [image, setImage] = useState<IFileData | null>(null);
|
||||
const [isLoading, setLoading] = useState(false);
|
||||
|
||||
/**
|
||||
* Handle submit form
|
||||
* Upload image (jika ada) dan submit data maps ke API
|
||||
*/
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// Prepare data tanpa image dulu
|
||||
let newData: any = {
|
||||
authorId: user?.id,
|
||||
portofolioId: id,
|
||||
namePin: name,
|
||||
latitude: selectedLocation?.latitude,
|
||||
longitude: selectedLocation?.longitude,
|
||||
};
|
||||
|
||||
// Upload image jika ada
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
// Submit ke API
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle image upload
|
||||
*/
|
||||
const handleImageUpload = async () => {
|
||||
try {
|
||||
await pickFile({
|
||||
allowedType: "image",
|
||||
setImageUri: (file) => {
|
||||
setImage(file);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.log("[MapsCreate] Image upload error:", error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Footer component dengan button submit
|
||||
*/
|
||||
const buttonFooter = (
|
||||
<BoxButtonOnFooter>
|
||||
<ButtonCustom
|
||||
isLoading={isLoading}
|
||||
disabled={!selectedLocation || name === ""}
|
||||
onPress={handleSubmit}
|
||||
>
|
||||
Simpan
|
||||
</ButtonCustom>
|
||||
</BoxButtonOnFooter>
|
||||
);
|
||||
|
||||
/**
|
||||
* Render screen dengan NewWrapper
|
||||
*/
|
||||
return (
|
||||
<NewWrapper footerComponent={buttonFooter}>
|
||||
<InformationBox text="Tentukan lokasi pin map dengan menekan pada map." />
|
||||
|
||||
<BaseBox>
|
||||
<MapSelectedPlatform
|
||||
selectedLocation={selectedLocation}
|
||||
onLocationSelect={(location) => {
|
||||
setSelectedLocation(location as LatLng);
|
||||
}}
|
||||
height={300}
|
||||
showUserLocation={true}
|
||||
showsMyLocationButton={true}
|
||||
/>
|
||||
</BaseBox>
|
||||
|
||||
<TextInputCustom
|
||||
required
|
||||
label="Nama Pin"
|
||||
placeholder="Masukkan nama pin maps"
|
||||
value={name}
|
||||
onChangeText={setName}
|
||||
/>
|
||||
|
||||
<Spacing height={50} />
|
||||
|
||||
<InformationBox text="Upload foto lokasi bisnis anda untuk ditampilkan dalam detail maps." />
|
||||
|
||||
<LandscapeFrameUploaded image={image?.uri} />
|
||||
|
||||
<ButtonCenteredOnly icon="upload" onPress={handleImageUpload}>
|
||||
Upload
|
||||
</ButtonCenteredOnly>
|
||||
|
||||
<Spacing height={50} />
|
||||
</NewWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
export default Maps_ScreenMapsCreate;
|
||||
228
screens/Maps/ScreenMapsEdit.tsx
Normal file
228
screens/Maps/ScreenMapsEdit.tsx
Normal file
@@ -0,0 +1,228 @@
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
import {
|
||||
BoxButtonOnFooter,
|
||||
ButtonCenteredOnly,
|
||||
ButtonCustom,
|
||||
InformationBox,
|
||||
LandscapeFrameUploaded,
|
||||
Spacing,
|
||||
TextInputCustom,
|
||||
ViewWrapper,
|
||||
} from "@/components";
|
||||
import CustomSkeleton from "@/components/_ShareComponent/SkeletonCustom";
|
||||
import { MapSelectedPlatform } from "@/components/Map/MapSelectedPlatform";
|
||||
import MapSelectedV2 from "@/components/Map/MapSelectedV2";
|
||||
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 { LatLng } 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 function Maps_ScreenMapsEdit() {
|
||||
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 handleLocationSelect = (location: LatLng | [number, number]) => {
|
||||
if (Array.isArray(location)) {
|
||||
// Android format: [longitude, latitude]
|
||||
setSelectedLocation({ latitude: location[1], longitude: location[0] });
|
||||
} else {
|
||||
// iOS format: LatLng
|
||||
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 = (
|
||||
<BoxButtonOnFooter>
|
||||
<ButtonCustom
|
||||
disabled={!data.namePin}
|
||||
onPress={handleSubmit}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
Update
|
||||
</ButtonCustom>
|
||||
</BoxButtonOnFooter>
|
||||
);
|
||||
|
||||
const initialRegion =
|
||||
data?.latitude && data?.longitude
|
||||
? {
|
||||
latitude: Number(data?.latitude),
|
||||
longitude: Number(data?.longitude),
|
||||
latitudeDelta: 0.1,
|
||||
longitudeDelta: 0.1,
|
||||
}
|
||||
: defaultRegion;
|
||||
|
||||
return (
|
||||
<ViewWrapper footerComponent={buttonFooter}>
|
||||
<InformationBox text="Tentukan lokasi pin map dengan menekan pada map." />
|
||||
|
||||
{/* <MapSelectedPlatform
|
||||
initialRegion={initialRegion}
|
||||
selectedLocation={selectedLocation}
|
||||
onLocationSelect={handleLocationSelect}
|
||||
height={400}
|
||||
/> */}
|
||||
|
||||
{!data || !data.latitude || !data.longitude ? (
|
||||
<CustomSkeleton height={200} />
|
||||
) : (
|
||||
<MapSelectedV2
|
||||
selectedLocation={[data.longitude, data.latitude]}
|
||||
onLocationSelect={(location: [number, number]) => {
|
||||
setData({
|
||||
...data,
|
||||
longitude: location[0],
|
||||
latitude: location[1],
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TextInputCustom
|
||||
required
|
||||
label="Nama Pin"
|
||||
placeholder="Masukkan nama pin maps"
|
||||
value={data?.namePin}
|
||||
onChangeText={(value) => setData({ ...data, namePin: value })}
|
||||
/>
|
||||
|
||||
<Spacing />
|
||||
|
||||
<InformationBox text="Upload foto lokasi bisnis anda untuk ditampilkan dalam detail maps." />
|
||||
<LandscapeFrameUploaded
|
||||
image={
|
||||
image
|
||||
? image?.uri
|
||||
: API_IMAGE.GET({ fileId: data?.imageId as string })
|
||||
}
|
||||
/>
|
||||
<ButtonCenteredOnly
|
||||
icon="upload"
|
||||
onPress={() => {
|
||||
pickFile({
|
||||
allowedType: "image",
|
||||
setImageUri(file) {
|
||||
setImage(file);
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
Upload
|
||||
</ButtonCenteredOnly>
|
||||
<Spacing height={50} />
|
||||
</ViewWrapper>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user