Fix create maps
iOS Project - HIPMIBadungConnect.xcodeproj/project.pbxproj Maps & Location Screens - screens/Maps/MapsView2.tsx - screens/Portofolio/BusinessLocationSection.tsx New Map Components - components/Map/MapsV2Custom.tsx - components/Map/SelectLocationMap.tsx ### No Issue
This commit is contained in:
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)
|
||||
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
112
components/Map/MapSelectedPlatform.tsx
Normal file
112
components/Map/MapSelectedPlatform.tsx
Normal file
@@ -0,0 +1,112 @@
|
||||
import { Platform } from "react-native";
|
||||
import MapSelected from "./MapSelected";
|
||||
import { MapSelectedV2 } from "./MapSelectedV2";
|
||||
import { Region } 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
|
||||
initialRegion={initialRegion as Region}
|
||||
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;
|
||||
163
components/Map/MapSelectedV2.tsx
Normal file
163
components/Map/MapSelectedV2.tsx
Normal file
@@ -0,0 +1,163 @@
|
||||
import React, { useCallback, useMemo, useRef } from "react";
|
||||
import {
|
||||
StyleSheet,
|
||||
View,
|
||||
ViewStyle,
|
||||
StyleProp,
|
||||
} from "react-native";
|
||||
import { MainColor } from "@/constants/color-palet";
|
||||
import {
|
||||
MapView,
|
||||
Camera,
|
||||
PointAnnotation,
|
||||
} from "@maplibre/maplibre-react-native";
|
||||
|
||||
const DEFAULT_MAP_STYLE = "https://tiles.openfreemap.org/styles/liberty";
|
||||
|
||||
export interface Region {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
latitudeDelta: number;
|
||||
longitudeDelta: number;
|
||||
}
|
||||
|
||||
export interface MapSelectedV2Props {
|
||||
initialRegion?: Region;
|
||||
selectedLocation?: [number, number];
|
||||
onLocationSelect?: (location: [number, number]) => void;
|
||||
height?: number;
|
||||
style?: StyleProp<ViewStyle>;
|
||||
mapViewStyle?: StyleProp<ViewStyle>;
|
||||
showUserLocation?: boolean;
|
||||
showsMyLocationButton?: boolean;
|
||||
mapStyle?: string;
|
||||
zoomLevel?: number;
|
||||
}
|
||||
|
||||
// ✅ Marker simple tanpa Animated — hapus pulse animation
|
||||
function SelectedLocationMarker({
|
||||
color = MainColor.darkblue,
|
||||
}: {
|
||||
size?: number;
|
||||
color?: string;
|
||||
}) {
|
||||
return (
|
||||
<View style={styles.markerContainer}>
|
||||
<View style={[styles.markerRing, { borderColor: color }]} />
|
||||
<View style={[styles.markerDot, { backgroundColor: color }]} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
export function MapSelectedV2({
|
||||
initialRegion,
|
||||
selectedLocation,
|
||||
onLocationSelect,
|
||||
height = 400,
|
||||
style = styles.container,
|
||||
mapViewStyle = styles.map,
|
||||
mapStyle,
|
||||
zoomLevel = 12,
|
||||
}: MapSelectedV2Props) {
|
||||
const defaultRegion = useMemo(
|
||||
() => ({
|
||||
latitude: -8.737109,
|
||||
longitude: 115.1756897,
|
||||
latitudeDelta: 0.1,
|
||||
longitudeDelta: 0.1,
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const region = initialRegion || defaultRegion;
|
||||
|
||||
// ✅ Simpan initial center — TIDAK berubah saat user tap
|
||||
const initialCenter = useRef<[number, number]>([
|
||||
region.longitude,
|
||||
region.latitude,
|
||||
]);
|
||||
|
||||
const handleMapPress = useCallback(
|
||||
(event: any) => {
|
||||
const coordinate = event?.geometry?.coordinates || event?.coordinates;
|
||||
if (coordinate && Array.isArray(coordinate) && coordinate.length === 2) {
|
||||
onLocationSelect?.([coordinate[0], coordinate[1]]);
|
||||
}
|
||||
},
|
||||
[onLocationSelect],
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={[style, { height }]} collapsable={false}>
|
||||
<MapView
|
||||
style={mapViewStyle}
|
||||
mapStyle={mapStyle || DEFAULT_MAP_STYLE}
|
||||
onPress={handleMapPress}
|
||||
logoEnabled={false}
|
||||
compassEnabled={true}
|
||||
compassViewPosition={2}
|
||||
compassViewMargins={{ x: 10, y: 10 }}
|
||||
scrollEnabled={true}
|
||||
zoomEnabled={true}
|
||||
rotateEnabled={true}
|
||||
pitchEnabled={false}
|
||||
>
|
||||
{/* ✅ Camera hanya set sekali di awal, tidak reactive ke selectedLocation */}
|
||||
<Camera
|
||||
defaultSettings={{
|
||||
centerCoordinate: initialCenter.current,
|
||||
zoomLevel: zoomLevel,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* ✅ Hanya render PointAnnotation jika ada selectedLocation */}
|
||||
{/* ✅ Key statis — tidak pernah unmount/remount */}
|
||||
{selectedLocation && (
|
||||
<PointAnnotation
|
||||
id="selected-location"
|
||||
key="selected-location"
|
||||
coordinate={selectedLocation}
|
||||
>
|
||||
<SelectedLocationMarker />
|
||||
</PointAnnotation>
|
||||
)}
|
||||
</MapView>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
width: "100%",
|
||||
backgroundColor: "#f5f5f5",
|
||||
overflow: "hidden",
|
||||
borderRadius: 8,
|
||||
},
|
||||
map: {
|
||||
flex: 1,
|
||||
},
|
||||
markerContainer: {
|
||||
width: 40,
|
||||
height: 40,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
// ✅ Ring statis pengganti pulse animation
|
||||
markerRing: {
|
||||
position: "absolute",
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 18,
|
||||
borderWidth: 2,
|
||||
opacity: 0.4,
|
||||
},
|
||||
markerDot: {
|
||||
width: 16,
|
||||
height: 16,
|
||||
borderRadius: 8,
|
||||
borderWidth: 2,
|
||||
borderColor: "#FFFFFF",
|
||||
},
|
||||
});
|
||||
|
||||
export default MapSelectedV2;
|
||||
@@ -163,6 +163,8 @@
|
||||
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) */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -635,6 +637,40 @@
|
||||
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\";
|
||||
";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
|
||||
@@ -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}
|
||||
|
||||
192
screens/Maps/ScreenMapsCreate.tsx
Normal file
192
screens/Maps/ScreenMapsCreate.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
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";
|
||||
import MapSelected from "@/components/Map/MapSelected";
|
||||
|
||||
/**
|
||||
* 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>
|
||||
{/* <MapSelected
|
||||
selectedLocation={selectedLocation as any}
|
||||
setSelectedLocation={setSelectedLocation}
|
||||
/> */}
|
||||
<MapSelectedPlatform
|
||||
selectedLocation={selectedLocation}
|
||||
onLocationSelect={(location) => {
|
||||
// Set location (auto handle LatLng format)
|
||||
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;
|
||||
Reference in New Issue
Block a user