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
|
- **Architecture**: File-based routing with Expo Router
|
||||||
- **State Management**: Context API (AuthContext)
|
- **State Management**: Context API (AuthContext)
|
||||||
- **UI Components**: React Native Paper, custom components
|
- **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
|
- **Push Notifications**: React Native Firebase Messaging
|
||||||
- **Build System**: Metro bundler
|
- **Build System**: Metro bundler
|
||||||
- **Package Manager**: Bun
|
- **Package Manager**: Bun
|
||||||
@@ -381,8 +381,8 @@ apiConfig.interceptors.request.use(async (config) => {
|
|||||||
- Push Notifications (FCM)
|
- Push Notifications (FCM)
|
||||||
- Configured for both iOS and Android
|
- Configured for both iOS and Android
|
||||||
|
|
||||||
### Mapbox
|
### Maplibre
|
||||||
- Map integration via `@rnmapbox/maps`
|
- Map integration via `@maplibre/maplibre-react-native`
|
||||||
- Location permissions configured
|
- Location permissions configured
|
||||||
|
|
||||||
### Deep Linking
|
### Deep Linking
|
||||||
@@ -475,10 +475,34 @@ rm -rf node_modules bun.lock
|
|||||||
bun install
|
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
|
## Documentation Files
|
||||||
|
|
||||||
- `docs/CHANGE_LOG.md` - Change log for recent updates
|
- `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/hipmi-note.md` - Build and deployment notes
|
||||||
- `docs/prompt-for-qwen-code.md` - Development prompts and patterns
|
- `docs/prompt-for-qwen-code.md` - Development prompts and patterns
|
||||||
|
|
||||||
@@ -488,3 +512,4 @@ bun install
|
|||||||
- [React Native Documentation](https://reactnative.dev/)
|
- [React Native Documentation](https://reactnative.dev/)
|
||||||
- [Expo Router Documentation](https://docs.expo.dev/router/introduction/)
|
- [Expo Router Documentation](https://docs.expo.dev/router/introduction/)
|
||||||
- [TypeScript Documentation](https://www.typescriptlang.org/docs/)
|
- [TypeScript Documentation](https://www.typescriptlang.org/docs/)
|
||||||
|
- [Maplibre React Native](https://github.com/maplibre/maplibre-react-native)
|
||||||
|
|||||||
@@ -1,143 +1,9 @@
|
|||||||
import {
|
import Maps_ScreenMapsCreate from "@/screens/Maps/ScreenMapsCreate";
|
||||||
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";
|
|
||||||
|
|
||||||
export default function MapsCreate() {
|
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 (
|
return (
|
||||||
<ViewWrapper footerComponent={buttonFooter}>
|
<>
|
||||||
<InformationBox text="Tentukan lokasi pin map dengan menekan pada map." />
|
<Maps_ScreenMapsCreate />
|
||||||
|
</>
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
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) */,
|
5A6E1555841A4B9D9D246E71 /* Remove signature files (Xcode workaround) */,
|
||||||
0B4282049A4A4293821DF904 /* Remove signature files (Xcode workaround) */,
|
0B4282049A4A4293821DF904 /* Remove signature files (Xcode workaround) */,
|
||||||
CCCF75FD0B87410193A6B7DB /* Remove signature files (Xcode workaround) */,
|
CCCF75FD0B87410193A6B7DB /* Remove signature files (Xcode workaround) */,
|
||||||
|
51F61F14096F4B7FBD9344A7 /* Remove signature files (Xcode workaround) */,
|
||||||
|
60F3AE3AC4B24C2FA7FC8F10 /* Remove signature files (Xcode workaround) */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@@ -635,6 +637,40 @@
|
|||||||
rm -rf \"$CONFIGURATION_BUILD_DIR/MapLibre.xcframework-ios.signature\";
|
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 */
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
|||||||
@@ -40,8 +40,12 @@ export default function DrawerMaps({
|
|||||||
closeDrawer={() => setOpenDrawer(false)}
|
closeDrawer={() => setOpenDrawer(false)}
|
||||||
height={"auto"}
|
height={"auto"}
|
||||||
>
|
>
|
||||||
<DummyLandscapeImage height={200} imageId={selected.imageId} />
|
{selected.imageId && (
|
||||||
<Spacing />
|
<>
|
||||||
|
<DummyLandscapeImage height={200} imageId={selected.imageId} />
|
||||||
|
<Spacing />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<StackCustom gap={"xs"}>
|
<StackCustom gap={"xs"}>
|
||||||
<GridTwoView
|
<GridTwoView
|
||||||
spanLeft={2}
|
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